Practical Middleware Patterns

Request IDs, Timers, Conditional Auth, and ctx.state — The Daily Toolkit

Practical Middleware Patterns

Real applications assemble middleware from a handful of recurring patterns. This lesson covers request-id injection, response-time headers, conditional auth short-circuiting, and enriching ctx.state for downstream handlers.

5 min read Level 3/5 #koa#middleware#patterns
What you'll learn
  • Add a unique request ID to every request via ctx.state and response header
  • Short-circuit a request early when a condition is not met
  • Enrich ctx.state in upstream middleware for downstream consumption

After working through the onion model, authoring, ordering, error handling, and composition, the final step is recognising the patterns that appear in almost every production Koa application.

Pattern 1 — Request ID

Attach a unique ID to every request so that log entries from different middleware can be correlated.

import { randomUUID } from 'node:crypto';

app.use(async (ctx, next) => {
  ctx.state.requestId = ctx.headers['x-request-id'] || randomUUID();
  ctx.set('X-Request-Id', ctx.state.requestId);
  await next();
});

Downstream middleware and handlers read ctx.state.requestId to include it in log lines and error payloads.

Pattern 2 — Response Time

app.use(async (ctx, next) => {
  const start = performance.now();
  await next();
  const ms = (performance.now() - start).toFixed(2);
  ctx.set('X-Response-Time', `${ms}ms`);
});

The downstream phase (after await next()) runs after the innermost handler has set ctx.body, so ms accurately reflects the full pipeline.

Pattern 3 — Conditional Auth (Short-Circuit)

Skip next() entirely to reject a request without running any further middleware.

const PUBLIC_PATHS = new Set(['/health', '/login', '/register']);

app.use(async (ctx, next) => {
  if (PUBLIC_PATHS.has(ctx.path)) {
    return next(); // pass through with no auth check
  }

  const token = ctx.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    ctx.status = 401;
    ctx.body = { error: 'Missing token' };
    return; // short-circuit — do NOT call next()
  }

  try {
    ctx.state.user = await verifyJwt(token);
  } catch {
    ctx.throw(401, 'Invalid token');
  }

  await next();
});

Pattern 4 — Decorating ctx.state

ctx.state is the idiomatic place to pass data between middleware. Treat it as a per-request store:

// Middleware: load feature flags
app.use(async (ctx, next) => {
  ctx.state.flags = await featureFlags.forUser(ctx.state.user?.id);
  await next();
});

// Route handler: use the flags
router.get('/dashboard', async (ctx) => {
  ctx.body = {
    newUI: ctx.state.flags.has('new-dashboard'),
    user: ctx.state.user,
  };
});

Pattern 5 — Combining Into a Reusable Stack

The patterns above work best when composed together (see the koa-compose lesson) so every app bootstraps with a single app.use(coreStack) call:

import compose from 'koa-compose';

export const coreStack = compose([
  requestId,
  responseTime,
  conditionalAuth,
  featureFlagLoader,
]);

Quick Reference

PatternKey propertyPhase
Request IDctx.state.requestIdUpstream
Response timectx.set('X-Response-Time', ...)Downstream
Conditional authreturn without next()Upstream
State decorationctx.state.*Upstream
Reusable stackcompose([...])Both

These five patterns cover the majority of middleware you will write or configure in a Koa application. Combine them with the ordering rules and error-handling strategy from earlier lessons to build a robust, predictable request pipeline.

Up Next

With middleware mastered, the next chapter moves into building RESTful APIs with @koa/router — routes, parameters, and resource design.

REST API Intro →