MVC (Model-View-Controller)

Separating Concerns in Web Applications

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.

What is MVC?

MVC divides an application into three interconnected components, each with a distinct responsibility:

  1. Model — manages data and business logic. It knows how to read from and write to the database, validate data, and enforce business rules. The Model has no knowledge of how data is displayed.
  2. View — handles presentation. It takes data from the Model and renders it into HTML, JSON, or whatever format the client needs. The View has no knowledge of how data is stored.
  3. Controller — handles user input and coordinates the flow. It receives requests, asks the Model for data or tells it to update, then passes the result to the View. The Controller is the traffic cop.
┌──────────────┐ │ Controller │ Request │ │ USER ─────────────────────▶ │ Handles │ │ input │ └──────┬───────┘ │ ┌──────────┴──────────┐ │ │ ▼ │ ┌─────────────┐ │ │ Model │ │ │ │ │ │ Data & │ ◀──────▶ Database │ Logic │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ View │ │ │ Response │ Renders │ ─────────────────▶ USER │ output │ └─────────────┘
MVC is a pattern, not a framework. You can implement it in any language with any tools. Express, Laravel, Django, and Rails all use MVC, but you can build MVC in plain Node.js or PHP without any framework at all. The pattern is about how you organize your code, not what tools you use.

Why Separate Concerns?

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:

Without MVC: Everything in One File

<?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:

With MVC: Separated Responsibilities

// ── 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.

Server-Side MVC

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.

Server-Side MVC Flow: ┌─────────┐ HTTP Request ┌──────────────┐ Query ┌─────────────┐ │ │ ──────────────────▶ │ │ ───────────▶ │ │ │ Browser │ │ Controller │ │ Model │ ◀──▶ Database │ │ │ │ ◀─────────── │ │ │ │ Complete HTML │ │ Data │ │ │ │ ◀────────────────── │ ┃ │ └─────────────┘ │ │ │ ▼ │ └─────────┘ │ ┌────────┐ │ │ │ View │ │ │ │ (EJS, │ │ │ │ PHP, │ │ │ │ Pug) │ │ │ └────────┘ │ └──────────────┘ Server

Step-by-Step Flow

  1. Browser sends request — user clicks a link or submits a form, sending an HTTP request (e.g., GET /users)
  2. Router dispatches to Controller — the routing layer matches the URL pattern and calls the appropriate controller method
  3. Controller asks Model for data — the controller calls model methods like User.getAll() or User.findById(id)
  4. Model queries database — the model executes SQL queries, processes results, and returns structured data to the controller
  5. Controller passes data to View — the controller selects the appropriate template and passes the model data to it
  6. View renders HTML — the template engine combines the data with HTML markup, producing a complete page that is sent back to the browser

File Structure

# 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.

When to Use Server-Side MVC

Frameworks

Framework Language Template Engine
Express + EJS Node.js EJS, Pug, Handlebars
Laravel PHP Blade
Rails Ruby ERB, Haml
Django Python Django Templates, Jinja2

Client-Side MVC (SPA Pattern)

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.

Client-Side MVC (SPA) Flow: ┌───────────────────────────┐ ┌──────────────────────────┐ │ Browser │ │ Server │ │ │ │ │ │ ┌─────────────────────┐ │ fetch() / XHR │ ┌────────────────────┐ │ │ │ JavaScript App │ │ ─────────────────▶ │ │ Controller │ │ │ │ │ │ │ │ │ │ │ │ ┌───────────────┐ │ │ │ │ Receives request │ │ │ │ │ View (React, │ │ │ │ │ Calls Model │ │ │ │ │ Vue, Angular)│ │ │ │ └────────┬───────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ Renders DOM │ │ │ JSON │ ┌────────▼───────────┐ │ │ │ │ in browser │ │ │ ◀───────────────── │ │ Model │ │ │ │ └───────────────┘ │ │ │ │ │ │ │ └─────────────────────┘ │ │ │ Queries DB │ │ │ │ │ │ Returns data │ │ └───────────────────────────┘ │ └────────────────────┘ │ │ │ │ Database ◀──▶ │ └──────────────────────────┘

Step-by-Step Flow

  1. Browser loads a minimal HTML shell — the initial page is mostly empty, with a <script> tag that loads the JavaScript application bundle
  2. JavaScript app initializes — the framework (React, Vue, Angular) boots up and takes control of the page
  3. App makes API request — the JavaScript calls fetch('/api/users') to request data from the server
  4. Server Controller processes request — the server receives the request, calls the Model, and returns JSON data
  5. Model queries database — the model fetches data and returns it; the controller sends it as a JSON response
  6. JavaScript renders the DOM — the client-side View receives the JSON and builds/updates the HTML in the browser using DOM manipulation

Key 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.

When to Use Client-Side MVC

Frameworks

Framework Approach
React Component-based, virtual DOM
Vue Reactive data binding, single-file components
Angular Full framework with dependency injection, RxJS
Accessibility and SEO tradeoff: If your app requires JavaScript to display any content, you've excluded users with JS disabled and made SEO harder. Search engine crawlers may not execute JavaScript, meaning your content is invisible to them. Server-side rendering (SSR) frameworks like Next.js and Nuxt.js exist specifically to address this problem. The Prof is very concerned about this approach as it is often a form of solving an architectural problem with even more complexity and may really suggest that you might have adopted a solution that is not the best fit for your situation.

Adaptable / Hybrid MVC (Progressive Enhancement)

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.

Adaptable MVC: Two Paths, One Backend Path A: JavaScript Enabled (Enhanced Experience) ┌──────────┐ fetch('/api/users') ┌────────────┐ ┌───────┐ │ Browser │ Accept: app/json │ │ Query │ │ │ (JS on) │ ────────────────────▶ │ Controller │ ─────────▶ │ Model │ ◀──▶ DB │ │ ◀──────────────────── │ │ ◀───────── │ │ │ Renders │ JSON response │ Checks │ Data └───────┘ │ via DOM │ │ Accept hdr │ └──────────┘ └────────────┘ Path B: No JavaScript (Baseline Experience) ┌─────────┐ GET /users ┌────────────┐ ┌───────┐ │ Browser │ Accept: text/html │ │ Query │ │ │ (JS off)│ ────────────────────▶ │ Controller │ ─────────▶ │ Model │ ◀──▶ DB │ │ ◀──────────────────── │ │ ◀───────── │ │ │ Displays│ Complete HTML page │ Checks │ Data └───────┘ │ HTML │ │ Accept hdr │ └─────────┘ └────────────┘

Detection: How Does the Controller Know?

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.

Node.js (Express) Example

// 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 Example

<?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';
}
?>
This is progressive enhancement applied to architecture. The app works without JavaScript — every page is a fully rendered HTML document that any browser can display. But when JavaScript is available, the experience is enhanced with dynamic updates, smoother transitions, and partial page refreshes. This connects directly to the content negotiation we covered in the REST overview.

REST is the API, Not the Backend

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).

REST is the Interface, MVC is the Organization: Client REST API MVC Backend Database ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ ┌───────────┐ │ │ │ │ │ │ │ │ │ Browser │ HTTP │ /api/users │ │ Controller │ │ │ │ Mobile │ ─────────▶ │ /api/items │ ────────▶ │ │ │ │ PostgreSQL│ │ Another │ │ /api/orders │ │ ▼ │ │ MongoDB │ │ Service │ ◀───────── │ │ ◀──────── │ Model ──▶ View │ ◀──▶ │ MySQL │ │ │ JSON/HTML │ Defines: │ │ │ │ │ └──────────┘ │ - URLs │ │ Organizes: │ └───────────┘ │ - Methods │ │ - Data logic │ │ - Status │ │ - Presentation │ │ codes │ │ - Flow control │ └──────────────┘ └──────────────────┘

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).

Don't confuse the API layer with the storage layer. You can swap PostgreSQL for MongoDB without changing your REST URLs. You can switch from Express to Fastify without changing your database queries. REST and MVC provide independent layers of abstraction, and that's the whole point of separating concerns.

MVC in Popular Frameworks

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/
users.js
controllers/
userController.js
models/
User.js
views/
users/index.ejs
Laravel (PHP) routes/
web.php
app/Http/
Controllers/UserController.php
app/Models/
User.php
resources/views/
users/index.blade.php
Django (Python) urls.py views.py models.py templates/
users/index.html
Rails (Ruby) config/
routes.rb
app/controllers/
users_controller.rb
app/models/
user.rb
app/views/
users/index.html.erb
Django's naming confusion: Django calls its pattern "MTV" (Model-Template-View) instead of MVC. In Django, what other frameworks call a "Controller" is called a "View," and what others call a "View" is called a "Template." The concepts are identical — only the names are different. Don't let the terminology trip you up: Django's views.py is the Controller, and its templates/ folder holds the Views.

Building a CRUD App

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.

Module 06: Database Setup

Set up PostgreSQL, create tables for user stories, and learn SQL fundamentals for CRUD operations.

  • PostgreSQL installation & configuration
  • CREATE TABLE, INSERT, SELECT, UPDATE, DELETE
  • Connecting from Node.js and PHP
Node.js PHP

Module 07: Server-Side MVC

Build a full CRUD app with server-rendered HTML pages. Forms submit data, controllers process it, views render the result.

  • Model, View, Controller file structure
  • Server-rendered forms and lists
  • POST/Redirect/GET pattern
Node.js PHP

Module 08: Client-Side MVC

Build the same CRUD app as a SPA. The server provides a JSON API, and JavaScript renders everything in the browser.

  • REST API serves JSON only
  • Vanilla JS fetch() for CRUD
  • Client-side DOM rendering
Node.js PHP

Node.js Demo PHP Demo

Module 09: Adaptable MVC

Combine both approaches. The same controller serves HTML or JSON based on what the client requests.

  • Content negotiation (Accept header)
  • Progressive enhancement
  • Works with and without JavaScript

Coming soon

Summary

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