If you've opened any backend codebase and seen folders named models/, views/, and controllers/, you've already met MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them.. It's a decades-old pattern that keeps showing up because it solves a real problem: where does each piece of code live? Understanding MVC gives you a mental map for reading and building server-side applications.
The three parts
MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them. stands for Model-View-Controller. Each part owns a specific slice of responsibility, and the rule is simple, don't let them bleed into each other.
Model
The Model is your data layer. It knows about your database, your business rules, and your validation logic. It does not know that an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request even happened.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
validate() {
if (!this.email.includes('@')) {
throw new Error('Invalid email');
}
if (this.name.length < 2) {
throw new Error('Name too short');
}
}
async save() {
this.validate();
await database.users.insert(this);
}
static async findById(id) {
const data = await database.users.findOne({ id });
return new User(data.name, data.email);
}
}View
The View renders HTML for the user. It receives data from the Controller and displays it. No database calls, no business logic, just presentation.
<!-- views/users/profile.ejs -->
<div class="user-profile">
<h1><%= user.name %></h1>
<p><%= user.email %></p>
<a href="/users/<%= user.id %>/edit">Edit profile</a>
</div>Controller
The Controller is the glue. It receives a request, calls the right Model methods, and hands the result to the right View. Thin controllers are good controllers.
class UserController {
async getProfile(req, res) {
const user = await User.findById(req.params.id);
res.render('users/profile', { user });
}
async updateProfile(req, res) {
const user = await User.findById(req.params.id);
user.name = req.body.name;
await user.save();
res.redirect('/profile');
}
}How data flows through MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them.
When a user clicks "Edit Profile", here is the chain of events:
1. Browser sends request to /users/42
2. Router matches and calls UserController.getProfile
3. Controller calls User.findById(42)
4. Model queries the database, returns a User object
5. Controller calls res.render('profile', { user })
6. View renders HTML using user data
7. Browser displays the pageNothing in step 3 knows about HTML. Nothing in step 6 knows about the database. That separation is the whole point.
MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them. across frameworks
| Framework | Model | View | Controller |
|---|---|---|---|
| Express.js | models/ | views/ (.ejs) | controllers/ |
| Django | models.py | templates/ | views.py (confusingly named) |
| Ruby on Rails | models/ | views/ (.erb) | controllers/ |
| Laravel | app/Models/ | resources/views/ | app/Http/Controllers/ |
Modern variations
Modern full-stack apps often split the V out entirely. Your backend becomes an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. that returns JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it., and a separate React or Vue frontend handles all the views.
Backend (API):
Models ✓
Controllers ✓
Views ✗ (returns JSON instead)
Frontend (React):
Components handle all presentationThis pattern is sometimes called MVVM (Model-View-ViewModel), where React components and state managers like Redux act as the ViewModel layer. The core principle, separate data from presentation, stays the same.
The fat controller trap
// Bad: business logic leaking into the controller
app.post('/users', async (req, res) => {
if (!req.body.email.includes('@')) return res.status(400).json({ error: 'Invalid email' });
if (req.body.name.length < 2) return res.status(400).json({ error: 'Name too short' });
const hashed = await bcrypt.hash(req.body.password, 10);
// ...more logic...
});
// Good: controller stays thin, Model owns the rules
app.post('/users', async (req, res) => {
const user = new User(req.body);
await user.save(); // validation happens inside the model
res.json(user);
});Quick reference
| Part | Owns | Does NOT own |
|---|---|---|
| Model | DB queries, validation, business rules | HTML rendering, request parsing |
| View | HTML templates, formatting | Database calls, business logic |
| Controller | Request handling, routing logic | Business rules, raw SQL |
// Complete MVC example (Express.js)
// models/User.js
class User {
constructor(data) {
this.id = data.id;
this.name = data.name;
this.email = data.email;
}
validate() {
if (!this.email.includes('@')) {
throw new Error('Invalid email');
}
if (this.name.length < 2) {
throw new Error('Name too short');
}
}
async save() {
this.validate();
// Save to database
await database.users.insert(this);
}
static async findById(id) {
const data = await database.users.findOne({ id });
return new User(data);
}
static async findAll() {
const data = await database.users.find();
return data.map(u => new User(u));
}
}
module.exports = User;
// controllers/userController.js
const User = require('../models/User');
exports.listUsers = async (req, res) => {
const users = await User.findAll();
res.render('users/index', { users });
};
exports.showUser = async (req, res) => {
const user = await User.findById(req.params.id);
res.render('users/show', { user });
};
exports.createUser = async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.redirect(`/users/${user.id}`);
} catch (error) {
res.render('users/new', { error: error.message });
}
};
// routes/userRoutes.js
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();
router.get('/users', userController.listUsers);
router.get('/users/:id', userController.showUser);
router.post('/users', userController.createUser);
module.exports = router;
// views/users/show.ejs
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<h1><%= user.name %></h1>
<p>Email: <%= user.email %></p>
<a href="/users/<%= user.id %>/edit">Edit</a>
</body>
</html>
// app.js
const express = require('express');
const userRoutes = require('./routes/userRoutes');
const app = express();
app.set('view engine', 'ejs');
app.use(express.json());
app.use(userRoutes);
app.listen(3000);