Two Parameters, One Await — That Is the Entire Middleware Contract
Writing Your Own Middleware
A Koa middleware is an async function that receives ctx and next. Learn the signature, when to call or skip next(), and how to build a real-world request-timer that injects an X-Response-Time header.
What you'll learn
- Write a correctly-shaped Koa middleware function
- Decide when to call, skip, or conditionally invoke next()
- Implement an X-Response-Time middleware using ctx.set()
Every Koa middleware shares the same shape: an async function that receives
two arguments — ctx (the context object) and next (a function that
invokes the rest of the pipeline).
The Signature
async function myMiddleware(ctx, next) {
// upstream: runs before inner layers
await next();
// downstream: runs after inner layers have finished
}
app.use(myMiddleware);
// or inline:
app.use(async (ctx, next) => { /* ... */ }); Calling next()
Calling await next() hands control to the next middleware in the stack.
If you skip it, no further middleware runs and the request ends with
whatever ctx.body is set to at that point (often undefined → 404).
// Short-circuit: reject unauthorized requests immediately
app.use(async (ctx, next) => {
if (!ctx.headers.authorization) {
ctx.status = 401;
ctx.body = { error: 'Unauthorized' };
return; // do NOT call next()
}
await next(); // continue to route handlers
}); Setting the Response
Use ctx.body to set the response body. Koa infers the Content-Type
from the value type (string → text/html, object/array → application/json).
app.use(async (ctx) => {
ctx.status = 200;
ctx.body = { message: 'ok' };
}); Real Example — X-Response-Time
A request timer is the classic “prove the onion works” middleware because it needs both upstream and downstream phases.
import Koa from 'koa';
const app = new Koa();
async function responseTime(ctx, next) {
const start = Date.now();
await next(); // downstream handlers run here
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`); // header added on the way out
}
app.use(responseTime);
app.use(async (ctx) => {
ctx.body = 'hello world';
});
app.listen(3000); The key insight: ctx.set() on the way out still reaches the client
because Koa hasn’t flushed the response yet when downstream code runs.
Middleware as a Named Function vs Arrow
Both work identically. Named functions make stack traces clearer:
// Arrow — concise, anonymous in stack traces
app.use(async (ctx, next) => { await next(); });
// Named — easier to debug
async function logger(ctx, next) {
console.log(ctx.method, ctx.url);
await next();
}
app.use(logger); Up Next
Middleware often needs to await database calls or external APIs — the
next lesson covers async patterns and the subtle bug of a missing await.