MVC (Model-View-Controller) is the most common pattern for organizing web applications. It separates data, presentation, and logic — making applications easier to understand, test, and modify.
MVC divides an application into three interconnected components, each with a distinct responsibility:
To understand why MVC matters, consider what happens without it. Here's a single PHP file that handles routing, database queries, and HTML rendering all in one place:
<?php
// routing, database, and HTML all mixed together
$pdo = new PDO('pgsql:host=localhost;dbname=app', 'user', 'pass');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$stmt = $pdo->query('SELECT * FROM users');
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head><title>Users</title></head>
<body>
<h1>User List</h1>
<table>
<?php foreach ($users as $user): ?>
<tr>
<td><?= htmlspecialchars($user['name']) ?></td>
<td><?= htmlspecialchars($user['email']) ?></td>
</tr>
<?php endforeach; ?>
</table>
</body>
</html>
<?php
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'];
$email = $_POST['email'];
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$name, $email]);
header('Location: /users.php');
}
?>
This works for a small script, but it becomes unmanageable as the application grows. Every file mixes SQL, HTML, and routing logic together. Now consider the same logic split into Model, View, and Controller:
// ── models/User.php ──
class User {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function getAll() {
$stmt = $this->pdo->query('SELECT * FROM users');
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function create($name, $email) {
$stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
return $stmt->execute([$name, $email]);
}
}
// ── views/users/index.php ──
<!DOCTYPE html>
<html>
<head><title>Users</title></head>
<body>
<h1>User List</h1>
<table>
<?php foreach ($users as $user): ?>
<tr>
<td><?= htmlspecialchars($user['name']) ?></td>
<td><?= htmlspecialchars($user['email']) ?></td>
</tr>
<?php endforeach; ?>
</table>
</body>
</html>
// ── controllers/UserController.php ──
class UserController {
private $userModel;
public function __construct($userModel) {
$this->userModel = $userModel;
}
public function index() {
$users = $this->userModel->getAll();
require 'views/users/index.php'; // $users is available in the view
}
public function store() {
$this->userModel->create($_POST['name'], $_POST['email']);
header('Location: /users');
}
}
The same functionality, but now each piece has one job. The benefits compound as the application grows:
| Benefit | How MVC Helps |
|---|---|
| Testability | You can test the Model (data logic) without rendering HTML, and test the View without a database. Each piece is testable in isolation. |
| Team Workflow | A frontend developer can work on Views while a backend developer works on Models. They don't step on each other's code. |
| Reusability | The same Model can serve an HTML view, a JSON API, and a CSV export. The same View template can display data from different Controllers. |
| Maintainability | Need to change how users are displayed? Edit the View. Need to change the database schema? Edit the Model. Neither change affects the other. |
In server-side MVC, the server does all the work: it processes the request, fetches data, renders HTML, and sends a complete page to the browser. The browser's job is simply to display what it receives.
GET /users)User.getAll() or User.findById(id)# Node.js with EJS
project/
├── app.js # Entry point, sets up Express
├── routes/
│ └── users.js # URL → controller mapping
├── controllers/
│ └── userController.js # Handles request/response logic
├── models/
│ └── User.js # Data access & business rules
└── views/
└── users/
├── index.ejs # List all users
├── show.ejs # Single user detail
└── form.ejs # Create/edit form
# PHP (manual MVC)
project/
├── index.php # Front controller (routes all requests)
├── .htaccess # Rewrites all URLs to index.php
├── controllers/
│ └── UserController.php # Handles request/response logic
├── models/
│ └── User.php # Data access & business rules
└── views/
└── users/
├── index.php # List all users
├── show.php # Single user detail
└── form.php # Create/edit form
Key characteristic: The browser receives complete HTML pages. There is no client-side rendering — the server does everything. When the user clicks a link, the browser makes a full page request and the server sends back a complete new page.
| Framework | Language | Template Engine |
|---|---|---|
| Express + EJS | Node.js | EJS, Pug, Handlebars |
| Laravel | PHP | Blade |
| Rails | Ruby | ERB, Haml |
| Django | Python | Django Templates, Jinja2 |
In client-side MVC (also called the Single-Page Application or SPA pattern), the server acts purely as a data API. It sends JSON, and the browser's JavaScript takes full responsibility for rendering the user interface.
<script> tag that loads the JavaScript application bundlefetch('/api/users') to request data from the serverKey characteristic: The server returns JSON data only. The client-side JavaScript is responsible for all rendering. Navigation between "pages" happens entirely in the browser without full page reloads.
| Framework | Approach |
|---|---|
| React | Component-based, virtual DOM |
| Vue | Reactive data binding, single-file components |
| Angular | Full framework with dependency injection, RxJS |
What if your application could work both ways? Serve complete HTML pages for browsers (and users with JavaScript disabled), but return JSON for JavaScript-powered clients? This is the adaptable MVC approach — the same controller handles both scenarios.
The controller inspects the request's Accept header (or the X-Requested-With header) to determine what format to respond with. This is the same content negotiation mechanism we covered in the REST overview.
// Express controller that adapts its response format
app.get('/users', async (req, res) => {
const users = await User.getAll();
if (req.accepts('html')) {
// Browser request → render full HTML page
res.render('users/index', { users });
} else {
// API / fetch() request → return JSON
res.json(users);
}
});
app.post('/users', async (req, res) => {
const user = await User.create(req.body);
if (req.accepts('html')) {
// Form submission → redirect to user list
res.redirect('/users');
} else {
// API call → return created user as JSON
res.status(201).json(user);
}
});
<?php
// PHP controller that adapts its response format
$users = $userModel->getAll();
$accept = $_SERVER['HTTP_ACCEPT'] ?? 'text/html';
$isAjax = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest';
if ($isAjax || str_contains($accept, 'application/json')) {
// JavaScript client → return JSON
header('Content-Type: application/json');
echo json_encode($users);
} else {
// Browser → render full HTML page
require 'views/users/index.php';
}
?>
A common misconception is that REST and MVC are competing patterns. They're not — they operate at different layers. REST defines how clients talk to your server (the API interface). MVC defines how your server is organized internally (the code structure).
Think of it this way:
You can have REST without MVC (a single-file API script), MVC without REST (a server-rendered app with no API), or both together (which is the most common real-world pattern).
Every major web framework organizes code using MVC (or something very close to it). The concepts are the same; only the file names and conventions differ:
| Framework | Routes | Controller | Model | View / Template |
|---|---|---|---|---|
| Express (Node.js) | routes/ |
controllers/ |
models/ |
views/ |
| Laravel (PHP) | routes/ |
app/Http/ |
app/Models/ |
resources/views/ |
| Django (Python) | urls.py |
views.py |
models.py |
templates/ |
| Rails (Ruby) | config/ |
app/controllers/ |
app/models/ |
app/views/ |
views.py is the Controller, and its templates/ folder holds the Views.
The best way to understand MVC is to build something with it. In the tutorials below, we'll build a complete CRUD application for managing User Stories — a common entity in project management. Each user story has a title, description, status, and priority.
We'll start by setting up the database (PostgreSQL), then build the same application three different ways: server-side MVC, client-side MVC (SPA), and an adaptable approach that combines both.
Set up PostgreSQL, create tables for user stories, and learn SQL fundamentals for CRUD operations.
Build a full CRUD app with server-rendered HTML pages. Forms submit data, controllers process it, views render the result.
Build the same CRUD app as a SPA. The server provides a JSON API, and JavaScript renders everything in the browser.
Combine both approaches. The same controller serves HTML or JSON based on what the client requests.
Coming soon
| Concept | Key Points |
|---|---|
| MVC | A pattern that separates an application into Model (data), View (presentation), and Controller (logic/flow). Not tied to any framework or language. |
| Model | Manages data and business rules. Knows how to talk to the database. Has no knowledge of how data is displayed. |
| View | Renders output (HTML, JSON, etc.). Takes data from the Model and formats it for the client. Has no knowledge of data storage. |
| Controller | Receives user input, coordinates the Model and View. The "traffic cop" that routes requests to the right code. |
| Server-Side MVC | Server renders complete HTML pages. Browser just displays them. Best for content sites, SEO, and progressive enhancement. Frameworks: Express+EJS, Laravel, Django, Rails. |
| Client-Side MVC | Server sends JSON, client JavaScript renders the UI. Best for highly interactive apps. Frameworks: React, Vue, Angular. Requires JS to function. |
| Adaptable MVC | Same controller serves HTML or JSON based on the Accept header. Progressive enhancement: works without JS, enhanced with it. |
| REST vs MVC | REST defines the external API interface (URLs, methods, status codes). MVC organizes internal server code. They are complementary, not competing. |
| ORMs | Object-Relational Mappers let Models interact with the database using objects instead of raw SQL. Examples: Sequelize (Node.js), Eloquent (Laravel), Django ORM, ActiveRecord (Rails). |
Back to Home | Database Overview | REST Overview | Node.js Tutorial | PHP Tutorial