Topic 8: Adding Middleware
📖 13 min read · 🎯 intermediate · 🧭 Prerequisites: react-lists, sessions-login-page-logout-session-destroy-timeoutcookies-introduction-page-redirect
Why this matters
Up until now, every Express route you've written does one job — receive a request, send a response. But real applications need things to happen in between: log every request hitting the server, parse incoming JSON, catch errors before they crash the app, serve static files. You don't want to copy that logic into every single route handler. That's where middleware comes in — it's code that runs before your route handler fires, shared across every request. Today we'll wire up application-level logging, static file serving, router-level isolation, and error-handling middleware to a live Express API.
What You'll Learn
- What middleware functions are and how they fit into the Express request-response cycle
- How to add application-level middleware (logging, JSON parsing, static files)
- How to add router-level middleware to isolate logic for specific route groups
- How to write error-handling middleware with the four-argument
(err, req, res, next)signature - How to trigger and test error-handling middleware from a route
The Analogy
Think of an Express application as an airport security checkpoint. Every passenger (request) must pass through a series of stations — ID check, bag scan, boarding-pass verification — before reaching the gate (the route handler). Each station is a middleware function: it can inspect the passenger, add a stamp to their boarding pass, send them to a secondary screening room (error handling), or wave them forward with next(). Skip a station and you lose the guarantee that the passenger is safe to board. The order of stations matters — you check the ID before the bag, not after.
Chapter 1: Introduction to Middleware
Middleware functions are functions that have access to three objects passed by Express:
req— the incoming request objectres— the outgoing response objectnext— a function that, when called, hands control to the next middleware in the stack
Four key kinds of middleware in Express:
- Middleware Function — any function that executes during the request-response cycle and calls
next()(or terminates the response itself). - Application-Level Middleware — bound to the Express
appinstance viaapp.use()orapp.METHOD(). Runs for every matching request. - Router-Level Middleware — bound to an instance of
express.Router(). Runs only for requests that match the router's mount path. - Error-Handling Middleware — distinguished by its four-argument signature
(err, req, res, next). Express only routes to it when an error is passed tonext(err).
graph LR
Request --> AppMiddleware["App-Level Middleware\n(logging, parsing, static)"]
AppMiddleware --> RouterMiddleware["Router-Level Middleware\n(route-specific logic)"]
RouterMiddleware --> RouteHandler["Route Handler\n(business logic)"]
RouteHandler --> Response
RouterMiddleware -- "next(err)" --> ErrorMiddleware["Error-Handling Middleware\n(err, req, res, next)"]
ErrorMiddleware --> Response
Chapter 2: Setting Up the Project
The class built a simple books API to serve as the playground for every middleware example that followed.
Step 1: Create a New Node.js Project
mkdir middleware-demo
cd middleware-demo
npm init -y
npm install express
Step 2: Create the 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 live at http://localhost:3000. You can interact with it using Postman, curl, or a web form.
Chapter 3: Adding Application-Level Middleware
Application-level middleware runs on every request that passes through the app instance. The class added two pieces: a request logger and a static-file server.
Step 1: Logging Middleware
Placed before route definitions so every request is logged regardless of which route handles it.
server.js:
const express = require('express');
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 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 to the console before the route handler runs.
Step 2: Middleware for Handling Static Files
express.static serves files from a directory on disk. Here the class points it at a public/ folder — any file placed there (HTML, CSS, images) is automatically served.
server.js:
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
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}`);
});
Drop an index.html into public/ and visit http://localhost:3000/ — Express serves it automatically.
Chapter 4: Adding Router-Level Middleware
Router-level middleware is identical in concept to application-level middleware, but it is bound to an express.Router() instance instead of app. This lets you group related routes — and their shared middleware — into their own files.
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 for the 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: Use the Router in the Main Server File
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 }));
// Use the books router
app.use('/books', booksRouter);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Now all /books routes live in routes/books.js. The app-level logger and the router-level logger both fire for every /books request — you'll see two log lines per hit.
Chapter 5: Adding Error-Handling Middleware
Express identifies error-handling middleware by its four-argument signature: (err, req, res, next). It must be registered after all routes and other middleware so it can catch errors forwarded by next(err).
Step 1: Create Error-Handling Middleware
server.js (final version):
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 }));
// Use the books router
app.use('/books', booksRouter);
// Error-handling middleware (must be 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: Trigger an Error
The class added a dedicated /error route inside routes/books.js to prove the error handler fires correctly.
routes/books.js (with error route added):
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
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 calls next(new Error('This is a test error')), which skips all remaining regular middleware and lands in the error handler. The server responds with HTTP 500 and logs the full stack trace to the console.
🧪 Try It Yourself
Task: Add a request-timing middleware to the books API that measures how long each request takes and appends the duration to the log output.
Success criterion: After every request to any /books route, your terminal should print a line like:
GET /books — 4ms
Starter snippet — drop this into server.js above the existing logging middleware:
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} — ${duration}ms`);
});
next();
});
Run the server, hit GET /books, and confirm the timing line appears in your terminal. Then try a POST /books with a JSON body and verify that too gets timed.
🔍 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 restarts the Express server
D) It logs the request to a file
Q2. Given the following snippet, what happens when a GET /books request arrives?
app.use((req, res, next) => {
console.log('A');
next();
});
app.use((req, res, next) => {
console.log('B');
// next() NOT called
});
app.get('/books', (req, res) => {
console.log('C');
res.send('books');
});
A) A, then B, then C are logged and the response is sent
B) Only A is logged; the request times out
C) A and B are logged; the request hangs (no response sent, C never runs)
D) B and C are logged; A is skipped
Q3. What distinguishes an error-handling middleware from a regular middleware in Express?
A) It must be registered before all routes
B) It uses app.error() instead of app.use()
C) It takes four arguments: (err, req, res, next)
D) It can only handle 404 errors
Q4. You want middleware that logs requests only for routes under /api/users, not for /api/products. How would you structure this?
A) Use app.use() at the top of the file with an if statement inside
B) Create an express.Router() for user routes, attach a router.use() logger, and mount it at /api/users
C) Add app.use('/api/users', logger) and app.ignore('/api/products', logger)
D) Middleware cannot be scoped to specific route prefixes in Express
A1. B — calling next() tells Express to move to the next function in the middleware stack; without it the request stalls.
A2. C — A and B are logged, but because the second middleware never calls next(), Express never reaches the /books route handler. The client hangs.
A3. C — Express identifies error-handling middleware solely by the presence of four parameters. The name err is conventional but the count is what matters.
A4. B — router-level middleware is the idiomatic Express way to scope middleware to a route prefix. Mounting the router at /api/users means its router.use() logger only fires for requests that match that path.
🪞 Recap
- Middleware functions receive
req,res, andnext; callingnext()advances the request through the pipeline. - Application-level middleware (registered with
app.use()) runs for every matching request across the entire app. - Router-level middleware (registered with
router.use()) isolates logic to a specific route group, keeping the codebase modular. - Error-handling middleware requires exactly four arguments
(err, req, res, next)and must be registered after all routes. - Calling
next(new Error(...))anywhere in the pipeline skips normal middleware and jumps straight to the error handler.
📚 Further Reading
- Express.js Middleware Guide — the source of truth on all middleware types and registration patterns
- Express.js Error Handling — official deep-dive into the four-argument error handler and async error propagation
- Node.js
pathmodule docs — reference forpath.join(__dirname, ...)used withexpress.static - ⬅️ Previous: Sessions, Login, Logout & Cookies
- ➡️ Next: Building a HTTP Server with Node.js