Writing Your Own Middleware

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.

4 min read Level 2/5 #koa#middleware#ctx
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.

Async Middleware →