Composition & Order

The middleware stack is an ordered pipeline; mastering placement and composing sub-stacks is what separates tidy apps from spaghetti.

Express has no dependency graph and no magic — middleware runs top to bottom, in registration order, and each layer decides whether to end the response or call next(). Almost every "why is req.user undefined?" bug is really an ordering bug.

A dependable global order

  1. Observability first: request id, then logging, so every later log line is correlated.
  2. Security headers (helmet) and cors.
  3. Body parsers (express.json) — only what you need, with sane limits.
  4. Session / auth population.
  5. Routers.
  6. 404 fallback.
  7. Centralized error handler (four-arg), always dead last.

Composing sub-stacks

You do not have to register middleware one call at a time. An array is a first-class stack you can name, reuse and mount, which reads far better than five sequential app.use lines.

const protect = [authenticate, loadCurrentUser, requireVerifiedEmail];
app.get('/billing', protect, billing.show);

Example

Example · javascript
import express from 'express';
import helmet from 'helmet';
import { randomUUID } from 'node:crypto';

const app = express();

// 1. Observability — correlate everything that follows.
app.use((req, res, next) => {
  req.id = req.get('x-request-id') ?? randomUUID();
  res.set('x-request-id', req.id);
  next();
});

// 2. Security.
app.use(helmet());

// 3. Parsers, scoped and limited.
app.use('/api', express.json({ limit: '100kb' }));

// A reusable, named sub-stack composed as an array.
const adminOnly = [
  (req, res, next) => { req.user = { role: 'admin' }; next(); }, // pretend auth
  (req, res, next) => req.user?.role === 'admin'
    ? next()
    : res.status(403).json({ error: 'Forbidden' }),
];

app.get('/api/admin/stats', adminOnly, (req, res) => {
  res.json({ ok: true, requestId: req.id });
});

// 4. Order guarantees this only runs when nothing above matched.
app.use((req, res) => res.status(404).json({ error: 'Not found' }));

// 5. Error handler is ALWAYS last, with the four-arg signature.
app.use((err, req, res, next) => {
  res.status(err.status ?? 500).json({ error: err.message, requestId: req.id });
});

When to use it

  • A team composes an authentication check, a permission check, and a rate limiter into a single guardedRoute array that is applied to every sensitive endpoint.
  • A developer chains logging, body parsing, and CORS into a setupMiddleware function called once in app.js to keep bootstrap code readable.
  • A feature flag middleware is composed with a cache middleware so feature-flagged routes are only cached when the flag is enabled.

More examples

Composed middleware array

Defines a reusable adminGuard array that composes JWT validation, role checking, and rate limiting into a single middleware stack.

Example · js
const { requireJwt, requireRole, rateLimiter } = require('./middleware');

const adminGuard = [requireJwt, requireRole('admin'), rateLimiter(50)];

app.get('/admin/users',    adminGuard, ctrl.listUsers);
app.delete('/admin/users/:id', adminGuard, ctrl.deleteUser);

Bootstrap composition function

Isolates all global middleware registration in an applyMiddleware helper, keeping app.js clean and testable.

Example · js
function applyMiddleware(app) {
  app.use(require('helmet')());
  app.use(require('cors')({ origin: process.env.CLIENT_URL }));
  app.use(require('express').json({ limit: '10kb' }));
  app.use(require('morgan')('combined'));
}

const app = require('express')();
applyMiddleware(app);
app.use('/api', require('./routes'));

Composing with pipe-style function

A compose utility chains an arbitrary list of middlewares into a single function, mimicking Koa-style pipeline composition.

Example · js
function compose(...middlewares) {
  return (req, res, next) => {
    function dispatch(i) {
      if (i === middlewares.length) return next();
      middlewares[i](req, res, () => dispatch(i + 1));
    }
    dispatch(0);
  };
}

const pipeline = compose(requireJwt, requireRole('editor'), auditLog);
app.post('/posts', pipeline, postController.create);

Discussion

  • Be the first to comment on this lesson.