Express middleware is a series of functions that execute sequentially during the request-response cycle, allowing you to process and modify requests before they reach their final route handler.

Let’s trace an incoming HTTP request for /users/123 to an Express application.

const express = require('express');
const app = express();

// Middleware 1: Logging
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // Pass control to the next middleware/route
});

// Middleware 2: Authentication (simulated)
app.use('/users', (req, res, next) => {
  if (req.headers['x-auth-token'] === 'supersecret') {
    req.user = { id: 'user456', role: 'admin' }; // Attach user info
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
});

// Route for GET /users/:id
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  const authenticatedUser = req.user; // Access attached user info

  if (authenticatedUser && authenticatedUser.role === 'admin') {
    res.send(`Admin viewing user: ${userId}`);
  } else {
    res.send(`Public viewing user: ${userId}`);
  }
});

// Catch-all for 404s
app.use((req, res) => {
  res.status(404).send('Not Found');
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

When a client sends a GET request to http://localhost:3000/users/123 with the header X-Auth-Token: supersecret:

  1. Request Arrives: The server receives the request.
  2. Middleware 1 (Logging): app.use((req, res, next) => { ... }) is executed because it matches all requests. It logs [YYYY-MM-DDTHH:MM:SS.sssZ] GET /users/123 and then calls next().
  3. Middleware 2 (Authentication): app.use('/users', (req, res, next) => { ... }) is executed because the path /users is a prefix of the request URL /users/123. The X-Auth-Token header is checked. It’s supersecret, so req.user is set to { id: 'user456', role: 'admin' }, and next() is called.
  4. Route Handler: app.get('/users/:id', (req, res) => { ... }) is executed because its method (GET) and path pattern (/users/:id) match the incoming request. req.params.id is 123, and req.user is { id: 'user456', role: 'admin' }. Since authenticatedUser.role === 'admin', the response Admin viewing user: 123 is sent.
  5. Response Sent: The response is sent back to the client. The 404 middleware is never reached.

If the X-Auth-Token header was missing or incorrect:

  1. Steps 1 and 2 are the same.
  2. Middleware 2 (Authentication): The if condition fails. Instead of calling next(), res.status(401).send('Unauthorized') is called.
  3. Response Sent: The server immediately sends a 401 Unauthorized response. No further middleware or route handlers are executed for this request.

The key to understanding middleware is the next() function. When next() is called, Express passes control to the next middleware function or route handler in the stack. If next() is not called, the request-response cycle effectively stops at that middleware, and it must send a response itself (like the 401 in the example).

Middleware can be applied globally (using app.use() without a path), to specific paths (using app.use('/path', middleware)), or directly to specific routes (by passing middleware functions as arguments before the final handler, e.g., app.get('/users/:id', authMiddleware, (req, res) => { ... })). The order in which you define middleware matters critically; it dictates the sequence of execution.

When middleware is defined with a path prefix, like app.use('/users', authMiddleware), it will only be considered for requests whose URL path starts with /users. This allows you to group related middleware and routes. Middleware functions can also modify the request (req) and response (res) objects, making them powerful for tasks like parsing request bodies, setting headers, or attaching user information.

The req.params object is populated by route patterns using colon syntax like :id. The req.query object holds parameters from the URL’s query string (e.g., /users?sort=name). Middleware that needs to access these populated values must execute after the router has processed the URL.

The final app.use((req, res) => { ... }) acts as a catch-all. It’s placed last because it should only execute if no preceding middleware or route handlers have sent a response. This is a common pattern for implementing 404 Not Found responses.

A crucial detail often overlooked is that middleware functions can also accept an err argument. If a preceding middleware or route handler calls next(err), Express will skip all regular middleware and jump directly to the first middleware defined with four arguments ((err, req, res, next)), effectively creating an error-handling middleware chain. This allows for centralized error management.

Want structured learning?

Take the full Express course →