await next() Yields Downstream Then Resumes Upstream — the Onion Model
Cascading Middleware
Koa's defining feature is cascading middleware where each function wraps the rest of the stack, enabling clean before/after hooks like request timing inside a single async function.
What you'll learn
- Explain the onion model of middleware execution in Koa
- Implement a request-timing middleware using await next()
- Understand the difference between downstream and upstream execution
Cascading middleware is the feature that makes Koa different from almost every
other Node framework. Because next is a promise, a middleware function can
do work before the rest of the stack, yield control with await next(), and
then continue after all downstream middleware have completed — in the same
function, with no callbacks.
The Onion Model
Picture the middleware stack as concentric layers of an onion. A request
enters at the outermost layer and travels inward (downstream) through each
await next() call. After the innermost layer runs, control flows back
outward (upstream) through each function’s code after its await next().
┌─── Middleware A ───────────────────┐
│ ┌─── Middleware B ────────────┐ │
│ │ ┌─── Middleware C ─────┐ │ │
Request──┼──►│──►│ │ │ │
│ │ └─── Middleware C ─────┘ │ │
Response─┼──◄│──◄│ │ │ │
│ └─── Middleware B ────────────┘ │
└─── Middleware A ───────────────────┘
Request Timing Example
A practical use of the onion model is measuring how long a request takes to process:
import Koa from "koa";
const app = new Koa();
// Timing middleware — wraps everything below it
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // downstream runs here
const ms = Date.now() - start;
ctx.set("X-Response-Time", `${ms}ms`);
console.log(`${ctx.method} ${ctx.url} — ${ms}ms`);
});
// Logger middleware
app.use(async (ctx, next) => {
console.log(`Handling ${ctx.path}`);
await next();
});
// Response middleware
app.use(async (ctx) => {
ctx.body = { status: "ok" };
});
app.listen(3000); For every request the execution order is:
- Timing middleware: record
start - Logger middleware: log the path
- Response middleware: set
ctx.body - Logger middleware: (nothing after
next) - Timing middleware: calculate elapsed time, set header, log
Why This Matters
In Express, achieving the same timing result requires either a response-event
hook or a monkey-patched res.end. In Koa it is just two lines around
await next() — and the control flow is obvious from reading the code.
Up Next
See how async/await makes error handling and control flow far cleaner than Express-style callbacks.
Async Flow and Error Handling →