Middleware Deep Dive

Difficulty: Intermediate

Question

Explain the middleware pattern in Express in depth. What are the different types of middleware and how does the execution flow work?

Answer

Middleware functions are the backbone of Express. Every function that has access to req, res, and next is middleware. They execute sequentially in the order they are registered, forming a pipeline that each request flows through.

There are five types: application-level (app.use), router-level (router.use), error-handling (4 parameters), built-in (express.json, express.static), and third-party (cors, helmet, morgan). Each serves a different purpose but follows the same pattern.

The execution flow is: request enters -> global middleware runs in order -> matching route middleware runs -> handler executes -> response sent. If any middleware calls next(error) or throws, Express skips to the nearest error-handling middleware.

Middleware can end the request-response cycle by sending a response, or pass control to the next middleware by calling next(). If a middleware neither sends a response nor calls next(), the request hangs indefinitely. This is a common bug.

Code examples

Middleware Types and Execution Order

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

// 1. Application-level middleware (runs on ALL requests)
app.use((req, res, next) => {
  req.requestTime = Date.now();
  console.log(`[${req.method}] ${req.url}`);
  next(); // MUST call next() or send response
});

// 2. Built-in middleware
app.use(express.json());

// 3. Third-party middleware
app.use(cors());
app.use(helmet());

// 4. Path-specific middleware
app.use('/api', (req, res, next) => {
  console.log('API request');
  next();
});

// 5. Route-level middleware chain
app.get('/api/admin',
  authenticate,      // middleware 1
  authorize('admin'), // middleware 2
  (req, res) => {     // final handler
    res.json({ admin: true });
  }
);

// 6. Error-handling middleware (4 params - MUST be last)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: err.message });
});

Middleware runs in registration order. Error middleware has 4 params and catches any next(err) calls. It must be registered after all routes.

Custom Middleware Factory Pattern

// Middleware factory - returns middleware with custom config
function rateLimit({ windowMs, max }) {
  const requests = new Map();

  return (req, res, next) => {
    const ip = req.ip;
    const now = Date.now();
    const windowStart = now - windowMs;

    // Clean old entries
    const hits = (requests.get(ip) || []).filter(t => t > windowStart);
    hits.push(now);
    requests.set(ip, hits);

    if (hits.length > max) {
      return res.status(429).json({
        error: 'Too many requests',
        retryAfter: Math.ceil(windowMs / 1000)
      });
    }

    res.set('X-RateLimit-Limit', max);
    res.set('X-RateLimit-Remaining', max - hits.length);
    next();
  };
}

// Usage
app.use('/api', rateLimit({ windowMs: 60000, max: 100 }));

Factory functions return configured middleware. This pattern is used by almost all third-party middleware: cors(), helmet(), morgan('dev').

Async Middleware and Error Forwarding

// Express 5: async errors are caught automatically
app.get('/users/:id', async (req, res) => {
  // If this throws, Express 5 catches it
  const user = await db.users.findById(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
});

// Express 4: must wrap async handlers manually
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
}));

// next() with error skips to error middleware
app.use((req, res, next) => {
  if (!req.headers.authorization) {
    return next(new AppError('Unauthorized', 401));
  }
  next();
});

Express 5 natively catches async errors. In Express 4, wrap async handlers or use express-async-errors. next(error) jumps straight to error middleware.

Key points

Concepts covered

Middleware Pattern, Application-Level, Router-Level, Error Middleware, Third-Party