MVC Architecture
Separating Concerns for Maintainable Code
Explanation
What is MVC?
MVC stands for Model-View-Controller, a design pattern that separates your application into three interconnected components. Think of it like a restaurant:
- Model = Kitchen (handles data and business logic)
- View = Dining room (what customers see)
- Controller = Waiter (takes requests, coordinates between kitchen and dining room)
┌─────────────────────────────────────────────────────────┐
│ USER │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ CONTROLLER │ │
│ │ - Handles requests │ │
│ │ - Calls Model for data │ │
│ │ - Passes data to View │ │
│ └───────────┬───────────────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────┐ ┌───────────────────┐ │
│ │ MODEL │ │ VIEW │ │
│ │ - Database │ │ - HTML/JSX │ │
│ │ - Business logic│ │ - Templates │ │
│ │ - Validation │ │ - UI components │ │
│ └───────────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘
Why MVC Matters
- Separation of Concerns: Each part has one job
- Testability: Test each layer independently
- Maintainability: Change one part without breaking others
- Team Collaboration: Frontend and backend can work in parallel
Demonstration
Example 1: Express.js MVC Structure
/my-app
├── /models
│ └── User.js
├── /views
│ └── users.ejs
├── /controllers
│ └── userController.js
├── /routes
│ └── userRoutes.js
└── server.js
Model (models/User.js)
// Handles data and business logic
class User {
static users = [];
static nextId = 1;
constructor(name, email) {
this.id = User.nextId++;
this.name = name;
this.email = email;
this.createdAt = new Date();
}
static findAll() {
return this.users;
}
static findById(id) {
return this.users.find(u => u.id === id);
}
static create(data) {
const user = new User(data.name, data.email);
this.users.push(user);
return user;
}
static delete(id) {
const index = this.users.findIndex(u => u.id === id);
if (index !== -1) {
this.users.splice(index, 1);
return true;
}
return false;
}
}
module.exports = User;
Controller (controllers/userController.js)
// Handles requests and responses
const User = require('../models/User');
const userController = {
// GET /users
index: (req, res) => {
const users = User.findAll();
res.render('users/index', { users });
},
// GET /users/:id
show: (req, res) => {
const user = User.findById(parseInt(req.params.id));
if (!user) {
return res.status(404).render('errors/404');
}
res.render('users/show', { user });
},
// POST /users
create: (req, res) => {
const { name, email } = req.body;
// Validation
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}
const user = User.create({ name, email });
res.redirect(`/users/${user.id}`);
},
// DELETE /users/:id
destroy: (req, res) => {
const deleted = User.delete(parseInt(req.params.id));
if (!deleted) {
return res.status(404).json({ error: 'User not found' });
}
res.redirect('/users');
}
};
module.exports = userController;
Routes (routes/userRoutes.js)
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.get('/', userController.index);
router.get('/:id', userController.show);
router.post('/', userController.create);
router.delete('/:id', userController.destroy);
module.exports = router;
View (views/users/index.ejs)
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<form action="/users" method="POST">
<input name="name" placeholder="Name" required>
<input name="email" type="email" placeholder="Email" required>
<button type="submit">Add User</button>
</form>
<ul>
<% users.forEach(user => { %>
<li>
<a href="/users/<%= user.id %>"><%= user.name %></a>
(<%= user.email %>)
</li>
<% }) %>
</ul>
</body>
</html>
Example 2: API-Only MVC (No Views)
// controllers/apiUserController.js
const User = require('../models/User');
const apiUserController = {
// GET /api/users
index: (req, res) => {
const users = User.findAll();
res.json({ data: users });
},
// POST /api/users
create: (req, res) => {
try {
const user = User.create(req.body);
res.status(201).json({ data: user });
} catch (error) {
res.status(400).json({ error: error.message });
}
},
// GET /api/users/:id
show: (req, res) => {
const user = User.findById(parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ data: user });
}
};
Key Takeaways:
- Model: Data + business logic (no HTTP awareness)
- View: Presentation only (no business logic)
- Controller: Coordinator (thin, delegates to Model)
- Keep controllers thin - heavy logic goes in Models or Services
Imitation
Challenge 1: Add Update Functionality
Task: Add an update method to the Model and Controller.
Solution
// Model
static update(id, data) {
const user = this.findById(id);
if (!user) return null;
Object.assign(user, data);
return user;
}
// Controller
update: (req, res) => {
const user = User.update(parseInt(req.params.id), req.body);
if (!user) {
return res.status(404).json({ error: 'Not found' });
}
res.json({ data: user });
}
Challenge 2: Add a Service Layer
Task: Extract business logic into a UserService class.
Solution
// services/UserService.js
class UserService {
static createUser(data) {
// Validation logic
if (!data.email.includes('@')) {
throw new Error('Invalid email');
}
return User.create(data);
}
static getUserWithPosts(id) {
const user = User.findById(id);
if (!user) return null;
const posts = Post.findByUserId(id);
return { ...user, posts };
}
}
Practice
Exercise 1: Blog MVC
Difficulty: Beginner
Build an MVC blog with:
- Post model (title, content, author)
- CRUD controller actions
- Views for list, single, create, edit
Exercise 2: E-commerce MVC
Difficulty: Advanced
Build a product catalog:
- Product and Category models
- ProductController with filtering
- Cart functionality
- Service layer for complex operations
Summary
What you learned:
- MVC pattern and why it matters
- Model: Data and business logic
- View: Presentation layer
- Controller: Request handler and coordinator
Next Steps:
- Read: Node.js Architecture
- Practice: Refactor a messy codebase to MVC
- Build: Full MVC application with authentication
