SE

Middleware pattern

Must-know concept45 minBeginner

Request pipelines: auth → logging → validation → handler.

Middleware is "Chain of Responsibility" wearing a web framework's clothes: a request walks through a stack of small functions, each one free to log, authenticate, validate, short-circuit, or pass along. Every backend framework is built on it.

The big idea

A request comes in, a response goes out, and in between there is a pipeline of small functions, each able to do something before the handler and something after.

Request
Logger
Auth
Rate limit
Validation
Handler
Response

The "onion" model

Each middleware can run code before and after the handler. The shape is an onion — you go inwards through the layers, the handler sits at the centre, then you walk back out.

type Middleware = (
  req: Request,
  next: () => Promise<Response>,
) => Promise<Response>;
 
const logger: Middleware = async (req, next) => {
  const start = Date.now();
  const res = await next();                       // walks inward
  console.log(`${req.method} ${req.url} -> ${res.status} (${Date.now() - start}ms)`);
  return res;                                     // …and back out
};
 
const auth: Middleware = async (req, next) => {
  if (!req.headers.get('authorization')) {
    return new Response('unauthorized', { status: 401 });   // short-circuit
  }
  return next();
};

Calling next() walks one layer deeper. Not calling it short-circuits the pipeline — useful for auth failures and rate limits.

Composing the pipeline

A tiny runner is all you need:

async function compose(
  middlewares: Middleware[],
  handler: (req: Request) => Promise<Response>,
  req: Request,
) {
  let i = -1;
  const dispatch = async (idx: number): Promise<Response> => {
    if (idx <= i) throw new Error('next() called twice');
    i = idx;
    if (idx === middlewares.length) return handler(req);
    return middlewares[idx](req, () => dispatch(idx + 1));
  };
  return dispatch(0);
}
 
await compose([logger, auth, rateLimit, validate], finalHandler, request);

This is essentially what Koa's source looks like.

Where each concern lives

Cross-cutting concerns are exactly what middleware is for. A typical production stack:

  1. Request ID + tracing

    Stamp every request with an ID; thread it into logs and downstream calls.

  2. Structured logging

    Log incoming + outgoing pair with status, latency, user.

  3. Authentication

    Resolve the caller. Fail fast with 401.

  4. Authorization

    Decide whether this caller can do this action. Fail with 403.

  5. Rate limiting

    Reject excessive traffic with 429.

  6. Body parsing + validation

    Parse JSON, validate against schema, return 400 on failure.

  7. Business handler

    The actual route — no boilerplate left, just domain logic.

  8. Error handler (outermost)

    Catch everything that escaped; map to status + body.

Beyond HTTP

The pattern shows up everywhere there's a request-shaped pipeline:

gRPC interceptors
Same shape, different protocol.
Message bus consumers
Each handler decorates the next; great for tracing + retries.
Database hooks
Pre/post-insert handlers (ORMs).
Redux middleware
Even client-side state updates.

In practice

The first instinct of new code is to stuff cross-cutting concerns into the route handler ("just log it here for now"). Resist. Build the middleware stack early. Every concern you extract becomes one less thing your handlers have to remember to do.

Key takeaways

  • Middleware is Chain of Responsibility for request pipelines.
  • Each middleware can act before and after the handler — the onion model.
  • Calling `next()` continues; not calling it short-circuits.
  • Order matters: logging outermost, error handler outermost, rate limit before auth.
  • Every cross-cutting concern that ends up in a handler is a middleware that wasn't.

Checkpoint questions

Use these to test whether the lesson is clear enough to explain without rereading.

  1. 1What happens before and after the handler in an onion-style middleware stack?
  2. 2Why does middleware order matter for auth, logging, validation, and error handling?
  3. 3Which concerns belong in middleware instead of the route handler?
  4. 4Where else do middleware-like pipelines appear outside HTTP frameworks?

References

External resources for going deeper after the lesson above.