Koa Middleware

Koa's Context and Cascading Middleware

Koa 304,877 comes from the Express 24,430 authors and drops almost everything — no router, no body parser, no static serving, no template engine — leaving two ideas.

The first is context. A middleware receives (ctx, next), and ctx carries both sides of the exchange: ctx.path, ctx.query, ctx.status, ctx.set(...) and ctx.body. Assigning ctx.body sends nothing; it records what should be sent, and Koa serializes it once every middleware has finished. The Node originals remain as ctx.req and ctx.res.

The second is the cascade. await next() suspends the current middleware until everything downstream finishes: code before the await runs down, code after it on the way back up.

The cascade in three middlewares (cascade.js)JavaScript
app.use(async (ctx, next) => {                    // 1 timer
  const t = process.hrtime.bigint(); console.log('1 timer in');
  await next();                                   // everything downstream runs here
  const ms = Number(process.hrtime.bigint() - t) / 1e6;
  ctx.set('x-response-time', `${ms.toFixed(1)}ms`);
  console.log(`1 timer out status=${ctx.status} ${ms.toFixed(1)}ms`);
});
app.use(async (ctx, next) => {                    // 2 error boundary
  try { await next(); } catch (err) {
    ctx.status = err.status ?? 500; ctx.body = { error: { message: err.message } };
    console.log(`2 errors out caught "${err.message}"`);
  }
});
app.use((ctx) => {                                // 3 handler, calls no next()
  if (ctx.path === '/boom') ctx.throw(418, 'I am a teapot');
  ctx.body = { data: { path: ctx.path } };
});
Output
1 timer in                              1 timer in
1 timer out status=200 17.2ms           2 errors out caught "I am a teapot"
200  x-response-time: 17.2ms            1 timer out status=418 2.1ms
{"data":{"path":"/books"}}              418  x-response-time: 2.1ms
                                        {"error":{"message":"I am a teapot"}}

Two things there are impossible in Express. The timer sets a header after the handler chose the body, because nothing has flushed yet; the Express equivalent monkey-patches res.end or reaches for on-headers. And the error boundary is a plain try/catch rather than a function recognized by arity — it catches the ctx.throw(418, ...) raised two layers down, after which the timer still runs.

Koa's cascade returns through every middleware; an Express stack only goes down
Koa's cascade returns through every middleware; an Express stack only goes down