Topic 26 of 56 ยท Full Stack Advanced

Topic 1 : The model-view-controller pattern

Lesson TL;DRTopic 1: The ModelViewController Pattern ๐Ÿ“– 7 min read ยท ๐ŸŽฏ beginner ยท ๐Ÿงญ Prerequisites: introductiontophpmyadmin, introductiontoreact Why this matters Up until now, you may have written all your code...
7 min readยทbeginnerยทmvc ยท nodejs ยท express ยท ejs

Topic 1: The Model-View-Controller Pattern

๐Ÿ“– 7 min read ยท ๐ŸŽฏ beginner ยท ๐Ÿงญ Prerequisites: introduction-to-phpmyadmin, introduction-to-react

Why this matters

Up until now, you may have written all your code in one place โ€” data, logic, and HTML jumbled together in a single file. That works for tiny experiments, but the moment your app grows, it becomes a nightmare to read, fix, or hand off to someone else. The Model-View-Controller pattern is how real Node.js and Express applications stay organized. It gives every piece of your code a clear home โ€” and once you see it, you'll wonder how you ever built without it.

What You'll Learn

  • What the three MVC components are and what each one is responsible for
  • How to scaffold an MVC directory structure inside a Node.js/Express project
  • How to wire together a Model, Controller, View, and Routes layer end-to-end
  • How to run the completed application and verify the full request/response cycle

The Analogy

Think of a restaurant. The kitchen (Model) stores the ingredients and follows recipes โ€” it knows what exists and how to prepare it, but it never speaks directly to customers. The dining room (View) is what customers see: plates, menus, decor โ€” pure presentation. The waitstaff (Controller) stands between the two: they take your order, relay it to the kitchen, then carry the finished plate back to your table. No matter how busy the restaurant gets, each role stays in its lane, which is exactly why a well-run kitchen can serve hundreds of covers without chaos.

Chapter 1: Introduction to the MVC Pattern

The Model-View-Controller (MVC) pattern divides an application into three main components. This separation manages complexity in large applications by promoting modularity and separation of concerns.

ComponentResponsibility
ModelRepresents the data and the business logic of the application
ViewRepresents the presentation layer, displaying data to the user
ControllerHandles user input and interactions, updating the Model and View accordingly

When a user submits a form, the request hits the Controller first. The Controller asks the Model for data (or tells it to change), then passes the result to the View to render a response. The Model and View never speak directly to each other.

sequenceDiagram
    participant Browser
    participant Controller
    participant Model
    participant View

    Browser->>Controller: HTTP Request (GET /books)
    Controller->>Model: getAllBooks()
    Model-->>Controller: books[]
    Controller->>View: render('books', { books })
    View-->>Browser: HTML Response

Chapter 2: Setting Up the MVC Structure

The class decided to build a simple book-listing web application using Node.js and Express to demonstrate the pattern in practice.

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 directly to an MVC responsibility: models/ owns data, views/ owns templates, controllers/ owns request logic, and routes/ owns the URL-to-controller mapping.

Chapter 3: Creating the Model

The Model represents the data and business logic. The class built a simple Book model backed by an in-memory array.

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
};

Three exported functions cover the three operations the app needs: read all, read one, and create. The Controller will call these; nothing else should.

Chapter 4: Creating the Controller

The Controller handles user input and updates the Model and View. It imports the Model and calls its functions, then hands off to the View via res.render().

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
};

Notice how createBook redirects after writing โ€” the classic Post/Redirect/Get pattern that prevents duplicate submissions on page refresh.

Chapter 5: Creating the View

The View is the presentation layer. The class used EJS (Embedded JavaScript Templates) to create HTML templates that receive data from the Controller and render it dynamically.

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 uses <% %> for logic (the forEach loop) and <%= %> to output escaped values. The View receives the books array as a plain JavaScript object โ€” it does not know or care where the data came from.

Chapter 6: Setting Up Routes

Routes define the URL endpoints of the application and map them to Controller functions. Keeping routes in their own file means app.js stays clean no matter how many endpoints you add.

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;

Three routes, three HTTP methods, three Controller functions โ€” a clean one-to-one mapping.

Chapter 7: Setting Up the Server

app.js is the entry point. It wires all the layers together: static files, body parsing middleware, the EJS view engine, and the route module.

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}`);
});

express.urlencoded parses the HTML form body so req.body.title and req.body.author are available inside createBook. Without it, those values would be undefined.

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

With all files in place, start the server:

node app.js

Open a browser and navigate to http://localhost:3000/books. You'll see the two seed books rendered in a list, and the form at the bottom lets you add new ones. Submitting the form fires a POST /books request, the Controller calls addBook, then redirects back to GET /books to display the updated list.

๐Ÿงช Try It Yourself

Task: Extend the book model with a deleteBook(id) function, then wire it to a new DELETE /books/:id route.

  1. Add deleteBook to models/bookModel.js:
function deleteBook(id) {
    books = books.filter(book => book.id !== id);
}
  1. Add a deleteBook handler to controllers/bookController.js:
function removeBook(req, res) {
    bookModel.deleteBook(parseInt(req.params.id));
    res.redirect('/books');
}
  1. Add the route to routes/bookRoutes.js:
router.post('/books/:id/delete', bookController.removeBook);
  1. Add a delete button to each <li> in views/books.ejs:
<% books.forEach(book => { %>
    <li>
        <%= book.title %> by <%= book.author %>
        <form action="/books/<%= book.id %>/delete" method="POST" style="display:inline">
            <button type="submit">Delete</button>
        </form>
    </li>
<% }) %>

Success criterion: Clicking Delete next to a book removes it from the list and the page reloads with that entry gone.

๐Ÿ” Checkpoint Quiz

Q1. In the MVC pattern, which component is responsible for receiving a user's form submission and deciding what to do next?

A) Model
B) View
C) Controller
D) Router

Q2. Given the following snippet from bookController.js, what happens when a request is made for /books/99 and no book with id 99 exists in the model?

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 page is rendered
C) The client receives a 404 response with the text "Book not found"
D) The client is redirected to /books

Q3. Why does app.js call app.use(express.urlencoded({ extended: true })) before registering routes?

A) To enable HTTPS on the server
B) To parse incoming HTML form data so req.body is populated in controllers
C) To serve static files from the public/ directory
D) To configure EJS as the view engine

Q4. You need to add a searchBooks(query) function that filters books by title. Which file should it live in, and why?

A1. C โ€” The Controller sits between the browser and the Model/View; it receives the HTTP request and orchestrates the response.

A2. C โ€” The else branch executes res.status(404).send('Book not found'), sending a 404 status code and that string as the body.

A3. B โ€” express.urlencoded is a body-parsing middleware. Without it, POST form data is not parsed and req.body remains undefined, breaking createBook.

A4. models/bookModel.js. The Model owns all data access and business logic. Filtering books by title is a data operation, not a presentation or routing concern. The Controller would then call bookModel.searchBooks(query) and pass results to the View.

๐Ÿชž Recap

  • MVC separates an application into three layers: Model (data/logic), View (presentation), and Controller (request handling).
  • In Express, routes map URLs to Controller functions; Controllers call Model functions and render Views.
  • EJS templates receive plain JavaScript objects from the Controller and use <% %> for logic and <%= %> for output.
  • express.urlencoded middleware must be registered before routes to make req.body available for POST requests.
  • Keeping each concern in its own folder makes the codebase easier to extend, test, and hand off to teammates.

๐Ÿ“š Further Reading

Like this topic? Itโ€™s one of 56 in Full Stack Advanced.

Block your seat for โ‚น2,500 and join the next cohort.