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.
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
| Pattern | Key property | Phase |
|---|---|---|
| Request ID | ctx.state.requestId | Upstream |
| Response time | ctx.set('X-Response-Time', ...) | Downstream |
| Conditional auth | return without next() | Upstream |
| State decoration | ctx.state.* | Upstream |
| Reusable stack | compose([...]) | 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.