Topic 8: Adding Middleware
📖 13 min read · 🎯 advanced · 🧭 Prerequisites: crud-operations, multi-processing-in-nodejs
Why this matters
Here's the thing — every request that hits your Express.js server doesn't jump straight to your route handler. It travels through a chain of functions first, each one doing a small job: parsing the request body, logging the incoming traffic, checking authentication. That chain is middleware. Without it, your routes receive raw, unprocessed requests — no body data, no logs, nothing. Today we're building those checkpoint functions ourselves, covering four types: application-level, router-level, error-handling, and static file serving. Miss even one, and your app starts behaving in ways that are hard to debug.
What You'll Learn
- What middleware functions are and how they fit into Express.js's request-response cycle
- How to write and register application-level middleware (logging, static files, body parsing)
- How to scope middleware to specific route groups using
express.Router() - How to write four-argument error-handling middleware and trigger it with
next(err)
The Analogy
Think of an Express app as a busy airport terminal. Every passenger (HTTP request) walks through a series of checkpoints before reaching their gate (route handler): first a security scanner (body parser), then a boarding-pass logger (logging middleware), then a gate agent (route handler). Each checkpoint can wave the passenger through by calling next(), redirect them, or detain them entirely by sending a response. The error-handling desk at the end of the terminal only activates when someone hands it a problem slip — the Error object passed to next(err). Skip any checkpoint and the passenger either arrives undocumented or crashes the whole system.
Chapter 1: Introduction to Middleware
Middleware functions are functions that have access to three things: the request object (req), the response object (res), and the next function. Calling next() passes control to the next middleware in the stack. Not calling it leaves the request hanging — a silent bug that baffles junior devs everywhere.
Four categories of Express middleware:
- Middleware Function — any function that executes during the request-response cycle with the signature
(req, res, next). - Application-Level Middleware — bound to an
express()instance viaapp.use()orapp.METHOD(); runs for every matching request. - Router-Level Middleware — bound to an
express.Router()instance; scoped to a sub-path and its routes. - Error-Handling Middleware — a four-argument function
(err, req, res, next); only invoked when an error is passed tonext(err).
graph LR
Client -->|HTTP request| LoggingMW["Logging Middleware"]
LoggingMW -->|next()| StaticMW["Static Files Middleware"]
StaticMW -->|next()| BodyMW["Body Parser Middleware"]
BodyMW -->|next()| Router["Router / Route Handler"]
Router -->|next(err)| ErrorMW["Error-Handling Middleware"]
Router -->|res.send()| Client
ErrorMW -->|res.status(500)| Client
Chapter 2: Setting Up the Project
Before layering on middleware, the class needed a working Express app to instrument.
Step 1 — Create a new Node.js project:
mkdir middleware-demo
cd middleware-demo
npm init -y
npm install express
Step 2 — Create the base server file (server.js):
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// CREATE: Add a new book
app.post('/books', (req, res) => {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(newBook);
res.status(201).json(newBook);
});
// READ: Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// READ: Get a book by ID
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// UPDATE: Update a book by ID
app.put('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// DELETE: Delete a book by ID
app.delete('/books/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 3 — Run the server:
node server.js
The API is now running at http://localhost:3000. Use Postman or any HTTP client to interact with it.
Chapter 3: Adding Application-Level Middleware
Application-level middleware is registered with app.use() and runs for every incoming request — or every request matching a given path prefix. Order matters: Express executes middleware top-to-bottom in registration order.
Step 1: Logging Middleware
Prepend a logging middleware before all other app.use() calls so every request gets recorded first.
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Logging middleware — must come before route handlers
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// CREATE: Add a new book
app.post('/books', (req, res) => {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(newBook);
res.status(201).json(newBook);
});
// READ: Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// READ: Get a book by ID
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// UPDATE: Update a book by ID
app.put('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// DELETE: Delete a book by ID
app.delete('/books/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Every request now prints a line like GET /books or POST /books to the console before any handler runs.
Step 2: Static Files Middleware
express.static serves files from a directory on disk. Any file placed in ./public becomes directly accessible by URL — no route handler needed.
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
// Logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Middleware to serve static files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public')));
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// CREATE: Add a new book
app.post('/books', (req, res) => {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(newBook);
res.status(201).json(newBook);
});
// READ: Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// READ: Get a book by ID
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// UPDATE: Update a book by ID
app.put('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// DELETE: Delete a book by ID
app.delete('/books/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create a public/ directory and drop an index.html in it — it will be served automatically at http://localhost:3000/.
Chapter 4: Adding Router-Level Middleware
Router-level middleware works identically to application-level middleware except it is bound to an express.Router() instance instead of the app instance. This lets you group related routes and their middleware under a single path prefix, keeping server.js clean.
Step 1: Create a Router (routes/books.js)
const express = require('express');
const router = express.Router();
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// Logging middleware scoped only to book routes
router.use((req, res, next) => {
console.log(`Books Router: ${req.method} ${req.url}`);
next();
});
// CREATE: Add a new book
router.post('/', (req, res) => {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(newBook);
res.status(201).json(newBook);
});
// READ: Get all books
router.get('/', (req, res) => {
res.json(books);
});
// READ: Get a book by ID
router.get('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// UPDATE: Update a book by ID
router.put('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// DELETE: Delete a book by ID
router.delete('/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
module.exports = router;
Step 2: Mount the Router in server.js
const express = require('express');
const path = require('path');
const booksRouter = require('./routes/books');
const app = express();
const PORT = process.env.PORT || 3000;
// Logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Middleware to serve static files
app.use(express.static(path.join(__dirname, 'public')));
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Mount the books router at /books
app.use('/books', booksRouter);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Now all /books/* traffic flows through the router's own middleware chain. The router-level logger fires independently of (and after) the app-level logger, so you get two log lines per books request — useful for debugging routing issues.
Chapter 5: Adding Error-Handling Middleware
Express identifies error-handling middleware by its four-argument signature: (err, req, res, next). It only runs when a previous middleware or route handler calls next(err) with an Error object. Register it last, after all routes and routers.
Step 1: Add Error-Handling Middleware to server.js
const express = require('express');
const path = require('path');
const booksRouter = require('./routes/books');
const app = express();
const PORT = process.env.PORT || 3000;
// Logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Middleware to serve static files
app.use(express.static(path.join(__dirname, 'public')));
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Mount the books router
app.use('/books', booksRouter);
// Error-handling middleware — four arguments, registered last
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something went wrong!');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 2: Add an Error-Triggering Route to routes/books.js
const express = require('express');
const router = express.Router();
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// Logging middleware for the book routes
router.use((req, res, next) => {
console.log(`Books Router: ${req.method} ${req.url}`);
next();
});
// Route to trigger an error — passes Error to the error-handling middleware
router.get('/error', (req, res, next) => {
next(new Error('This is a test error'));
});
// CREATE: Add a new book
router.post('/', (req, res) => {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(newBook);
res.status(201).json(newBook);
});
// READ: Get all books
router.get('/', (req, res) => {
res.json(books);
});
// READ: Get a book by ID
router.get('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// UPDATE: Update a book by ID
router.put('/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// DELETE: Delete a book by ID
router.delete('/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
module.exports = router;
Navigating to http://localhost:3000/books/error triggers the next(new Error(...)) call. Express skips all normal middleware, jumps directly to the four-argument error handler, logs the stack trace, and returns a 500 response with Something went wrong!.
🧪 Try It Yourself
Task: Add a request-timing middleware to the books app that measures how long each request takes and prints it to the console.
Success criterion: After hitting GET http://localhost:3000/books, your terminal should show something like:
GET /books — 3ms
Starter snippet — place this in server.js before any route registrations:
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.url} — ${Date.now() - start}ms`);
});
next();
});
The res.on('finish', ...) event fires after the response is sent, so you capture the full round-trip duration. Try POST and DELETE requests — do they show different timings?
🔍 Checkpoint Quiz
Q1. What is the purpose of calling next() inside a middleware function?
A) It sends the HTTP response to the client
B) It passes control to the next middleware or route handler in the stack
C) It terminates the server process
D) It resets req.body to an empty object
Q2. Given this code, what happens when a request hits GET /books?
app.use((req, res, next) => {
console.log('middleware A');
// next() is NOT called
});
app.get('/books', (req, res) => {
res.json({ books: [] });
});
A) Both "middleware A" is logged and { books: [] } is returned
B) "middleware A" is logged, but the request hangs and no response is sent
C) The route handler runs and skips middleware A
D) Express throws an unhandled error
Q3. What is the distinguishing signature of an Express error-handling middleware that sets it apart from regular middleware?
A) It uses app.error() instead of app.use()
B) It takes four arguments: (err, req, res, next)
C) It must be the first middleware registered
D) It only works when NODE_ENV=production
Q4. You're building an authentication middleware that should only protect /api/* routes, not static files served from /public. Which approach correctly scopes the middleware?
A) app.use(authMiddleware) at the top of server.js
B) Register an express.Router() for /api, attach authMiddleware to it with router.use(), and mount it at app.use('/api', apiRouter)
C) Set app.set('auth', true) and check it inside each route handler
D) Pass authMiddleware as the second argument to every app.get() call individually
A1. B — next() hands control to the next function in the middleware chain. Without it, the request cycle stalls.
A2. B — Without next() in middleware A, the chain is broken. The request hangs indefinitely; no response is ever sent to the client.
A3. B — Express detects error-handling middleware specifically by its four-argument signature (err, req, res, next). It only activates when next(err) is called with a truthy error argument.
A4. B — Router-level middleware scopes perfectly to a path prefix. Mounting the router at /api means the auth check only runs for requests under that prefix, leaving /public and other routes unaffected.
🪞 Recap
- Middleware functions receive
(req, res, next)and must callnext()to continue the chain or send a response to end it. - Application-level middleware is registered with
app.use()and runs for all matching requests in registration order. express.static()serves files from a local directory with no route handler needed.- Router-level middleware is scoped to an
express.Router()instance, keepingserver.jslean by grouping related routes and their middleware. - Error-handling middleware uses four arguments
(err, req, res, next)and must be registered after all other routes; it activates only whennext(err)is called.
📚 Further Reading
- Express.js Middleware docs — the source of truth on all middleware categories and ordering rules
- Express.js Error Handling guide — covers synchronous vs. async error propagation and custom error classes
- morgan on npm — production-grade HTTP request logger middleware; drop-in replacement for the hand-rolled logger built here
- ⬅️ Previous: Multi-Processing in Node.js
- ➡️ Next: Building a HTTP Server with Node.js Using HTTP APIs