Topic 18 of 18 · Node Expert

Topic 8 : Adding middleware

Lesson TL;DRTopic 8: Adding Middleware 📖 13 min read · 🎯 advanced · 🧭 Prerequisites: crudoperations, multiprocessinginnodejs Why this matters Here's the thing — every request that hits your Express.js server d...
13 min read·advanced·express · middleware · nodejs · routing

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:

  1. Middleware Function — any function that executes during the request-response cycle with the signature (req, res, next).
  2. Application-Level Middleware — bound to an express() instance via app.use() or app.METHOD(); runs for every matching request.
  3. Router-Level Middleware — bound to an express.Router() instance; scoped to a sub-path and its routes.
  4. Error-Handling Middleware — a four-argument function (err, req, res, next); only invoked when an error is passed to next(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 call next() 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, keeping server.js lean 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 when next(err) is called.

📚 Further Reading

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

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