Topic 11 of 18 · Node Expert

Topic 1 : The model-view-controller pattern

Lesson TL;DRTopic 1: The ModelViewController Pattern 📖 7 min read · 🎯 beginner · 🧭 Prerequisites: introductionandfoundation Why this matters Up until now, you've probably written Node.js code where everything ...
7 min read·beginner·mvc · nodejs · express · ejs

Topic 1: The Model-View-Controller Pattern

📖 7 min read · 🎯 beginner · 🧭 Prerequisites: introduction-and-foundation

Why this matters

Up until now, you've probably written Node.js code where everything lives together — your database queries, your HTML, your route logic, all jumbled in one file. It works at first, but then you try to change one thing and three other things break. Sound familiar? That's what developers call "spaghetti code." The Model-View-Controller pattern — MVC — is the industry's answer to this. It cuts your Express app into three clean, independent layers, so each piece has one job and changing one doesn't accidentally wreck the others.

What You'll Learn

  • Understand the three components of MVC — Model, View, and Controller — and what each one owns
  • Scaffold an MVC directory structure for a Node.js/Express application
  • Implement a working Book model, controller, and EJS view wired together through Express routes
  • Run the complete application and verify data flows correctly from model to browser

The Analogy

Think of a restaurant. The kitchen (Model) holds all the food and knows the recipes — it has no idea who is sitting at the tables. The waitstaff (Controller) takes your order, walks it back to the kitchen, and carries the finished plate out to you. The dining room (View) is what you actually see and interact with: candles, menus, plated food — pure presentation. If the chef changes a recipe, the dining room doesn't need a renovation. If the décor changes, the kitchen keeps cooking the same way. Each part has one job, and they talk through well-defined handoffs.

Chapter 1: Introduction to the MVC Pattern

MVC (Model-View-Controller) is a design pattern that divides an application into three interconnected components. This separation manages the complexity of large applications by promoting modularity and separation of concerns.

ComponentResponsibility
ModelRepresents data and business logic
ViewRepresents the presentation layer — what the user sees
ControllerHandles user input, coordinates between Model and View

Key concepts to lock in before going further:

  1. Model — the single source of truth for data and the rules that govern it. It knows nothing about HTTP or HTML.
  2. View — a template that renders data handed to it. It knows nothing about where the data came from.
  3. Controller — the traffic cop. It receives a request, asks the Model for data, and passes that data to the View to render.
flowchart LR
    User -->|HTTP Request| Controller
    Controller -->|reads/writes| Model
    Model -->|returns data| Controller
    Controller -->|passes data| View
    View -->|rendered HTML| User

Chapter 2: Setting Up the MVC Structure

Step 1: Create a New Node.js Project

mkdir mvc-demo
cd mvc-demo
npm init -y
npm install express

Step 2: Set Up the Project Structure

mvc-demo/
├── controllers/
│   └── bookController.js
├── models/
│   └── bookModel.js
├── routes/
│   └── bookRoutes.js
├── views/
│   └── books.ejs
├── public/
│   └── styles.css
├── app.js
├── package.json
└── package-lock.json

Each folder maps to one MVC concern. Routes sit alongside controllers because routing is how Express hands a request to the right controller function — it is glue, not a fourth layer.

Chapter 3: Creating the Model

The Model owns the data and every operation on it. For this demo the class uses an in-memory array, but the same interface would work with a database later — the controller would never need to change.

models/bookModel.js

let books = [
    { id: 1, title: '1984', author: 'George Orwell' },
    { id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];

function getAllBooks() {
    return books;
}

function getBookById(id) {
    return books.find(book => book.id === id);
}

function addBook(book) {
    book.id = books.length + 1;
    books.push(book);
}

module.exports = {
    getAllBooks,
    getBookById,
    addBook
};

Notice that bookModel.js has zero knowledge of req, res, or any HTTP concept. It is pure data logic.

Chapter 4: Creating the Controller

The Controller imports the Model, retrieves or mutates data, then tells Express which View to render — or redirects as needed.

controllers/bookController.js

const bookModel = require('../models/bookModel');

function getBooks(req, res) {
    const books = bookModel.getAllBooks();
    res.render('books', { books });
}

function getBook(req, res) {
    const book = bookModel.getBookById(parseInt(req.params.id));
    if (book) {
        res.render('book', { book });
    } else {
        res.status(404).send('Book not found');
    }
}

function createBook(req, res) {
    const newBook = {
        title: req.body.title,
        author: req.body.author
    };
    bookModel.addBook(newBook);
    res.redirect('/books');
}

module.exports = {
    getBooks,
    getBook,
    createBook
};

Three functions, three responsibilities:

  • getBooks — fetches all books and renders the list view
  • getBook — fetches one book by URL param; returns 404 if missing
  • createBook — reads POST body, adds the book via the model, then redirects back to the list

Chapter 5: Creating the View

The View is a template — it receives data and emits HTML. The class chose EJS (Embedded JavaScript), which lets you drop <% %> tags into otherwise normal HTML.

views/books.ejs

<!DOCTYPE html>
<html>
<head>
    <title>Books</title>
    <link rel="stylesheet" href="/styles.css">
</head>
<body>
    <h1>Books</h1>
    <ul>
        <% books.forEach(book => { %>
            <li><%= book.title %> by <%= book.author %></li>
        <% }) %>
    </ul>
    <form action="/books" method="POST">
        <input type="text" name="title" placeholder="Title" required>
        <input type="text" name="author" placeholder="Author" required>
        <button type="submit">Add Book</button>
    </form>
</body>
</html>

EJS tag cheat sheet:

  • <% %> — executes JavaScript (no output)
  • <%= %> — evaluates and outputs the value (HTML-escaped)

The view never calls require. It only works with variables passed in by the controller.

Chapter 6: Setting Up Routes

Routes map HTTP verb + URL path to a specific controller function. Keeping routes in their own file means app.js stays uncluttered as the application grows.

routes/bookRoutes.js

const express = require('express');
const router = express.Router();
const bookController = require('../controllers/bookController');

router.get('/books', bookController.getBooks);
router.get('/books/:id', bookController.getBook);
router.post('/books', bookController.createBook);

module.exports = router;

The three routes handle:

  • GET /books — list all books
  • GET /books/:id — show one book by ID
  • POST /books — create a new book from a form submission

Chapter 7: Setting Up the Server

app.js is the entry point. It wires middleware, the view engine, and all route modules together.

app.js

const express = require('express');
const path = require('path');
const bookRoutes = require('./routes/bookRoutes');

const app = express();
const PORT = 3000;

// Middleware
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.urlencoded({ extended: true }));

// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// Routes
app.use('/', bookRoutes);

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

Key lines explained:

  • express.static — serves everything in /public (CSS, images, JS) at the root URL
  • express.urlencoded — parses form POST bodies so req.body is populated
  • app.set('view engine', 'ejs') — tells Express to use EJS when res.render() is called

CSS: public/styles.css

body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f4;
    margin: 0;
    padding: 20px;
}

h1 {
    color: #333;
}

ul {
    list-style-type: none;
    padding: 0;
}

li {
    background: #fff;
    margin: 5px 0;
    padding: 10px;
    border: 1px solid #ddd;
}

form {
    margin-top: 20px;
}

input, button {
    padding: 10px;
    margin: 5px 0;
}

Chapter 8: Running the Application

node app.js

Navigate to http://localhost:3000/books in a browser. You should see the two seed books rendered as a list with a form below. Submit the form with a new title and author — the page will redirect back to /books with the new entry appended to the list.

🧪 Try It Yourself

Task: Add a deleteBook function to the MVC stack — model, controller, and route.

  1. In models/bookModel.js, add:
function deleteBook(id) {
    books = books.filter(book => book.id !== id);
}

Export it alongside the others.

  1. In controllers/bookController.js, add:
function removeBook(req, res) {
    bookModel.deleteBook(parseInt(req.params.id));
    res.redirect('/books');
}

Export it.

  1. In routes/bookRoutes.js, add:
router.post('/books/:id/delete', bookController.removeBook);
  1. In views/books.ejs, add a delete form inside the forEach loop:
<form action="/books/<%= book.id %>/delete" method="POST" style="display:inline;">
    <button type="submit">Delete</button>
</form>

Success criterion: Clicking Delete next to a book removes it from the list immediately on redirect. The remaining books retain their display order.

🔍 Checkpoint Quiz

Q1. What is the primary reason for using the MVC pattern in a web application?

A) It makes the application run faster
B) It separates data, presentation, and control logic so each can change independently
C) It removes the need for a database
D) It lets you avoid writing CSS

Q2. Given this controller snippet, what happens when a request hits GET /books/99 and no book with id 99 exists?

function getBook(req, res) {
    const book = bookModel.getBookById(parseInt(req.params.id));
    if (book) {
        res.render('book', { book });
    } else {
        res.status(404).send('Book not found');
    }
}

A) The server crashes with an unhandled exception
B) An empty book object is rendered
C) The response sends a 404 status with the text "Book not found"
D) The request is redirected to /books

Q3. A teammate adds database queries directly inside views/books.ejs. Which MVC principle does this violate, and which file should those queries live in instead?

Q4. Which Express middleware line is required for req.body.title to be populated when a form is submitted via POST?

A) app.use(express.static('public'))
B) app.use(express.json())
C) app.use(express.urlencoded({ extended: true }))
D) app.set('view engine', 'ejs')

A1. B — MVC enforces separation of concerns: the Model, View, and Controller each evolve without forcing changes in the others.

A2. C — The else branch runs, setting the HTTP status to 404 and sending the plain-text string "Book not found".

A3. This violates the separation of concerns principle — specifically the rule that the View only renders data; it should never fetch or mutate it. The queries belong in the Model (models/bookModel.js), called from the Controller which then passes results to the view.

A4. C — express.urlencoded parses application/x-www-form-urlencoded bodies (the default encoding for HTML forms) and attaches the parsed key-value pairs to req.body.

🪞 Recap

  • MVC splits an application into Model (data/logic), View (templates/HTML), and Controller (request handling/coordination).
  • The Model has zero knowledge of HTTP; the View has zero knowledge of data sources — only the Controller bridges them.
  • Express routes map HTTP verb + path to a specific controller function, keeping app.js lean.
  • EJS templates use <%= %> to output values and <% %> to run logic without output.
  • express.urlencoded({ extended: true }) is required to read form POST data from req.body.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

Block your seat for ₹2,500 and join the next cohort.