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.
| Component | Responsibility |
|---|---|
| Model | Represents the data and the business logic of the application |
| View | Represents the presentation layer, displaying data to the user |
| Controller | Handles 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.
- Add
deleteBooktomodels/bookModel.js:
function deleteBook(id) {
books = books.filter(book => book.id !== id);
}
- Add a
deleteBookhandler tocontrollers/bookController.js:
function removeBook(req, res) {
bookModel.deleteBook(parseInt(req.params.id));
res.redirect('/books');
}
- Add the route to
routes/bookRoutes.js:
router.post('/books/:id/delete', bookController.removeBook);
- Add a delete button to each
<li>inviews/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.urlencodedmiddleware must be registered before routes to makereq.bodyavailable for POST requests.- Keeping each concern in its own folder makes the codebase easier to extend, test, and hand off to teammates.
๐ Further Reading
- Express.js official docs โ routing, middleware, and application setup reference
- EJS documentation โ full template syntax for the view layer used in this lesson
- MDN: MVC architecture โ a concise, framework-agnostic explanation of the pattern
- โฌ ๏ธ Previous: Introduction to React
- โก๏ธ Next: Configuring Express