All questions

Middlewares

Premium

Middlewares

Implement compose(middlewares) — an async middleware composer in the style of Koa. Each middleware is a function that receives a shared context object and a next continuation. Calling next() descends into the next middleware in the chain; awaiting next() lets the current middleware run code AFTER the downstream chain finishes. That downstream-then-upstream traversal is the "onion model" — control flows IN through every layer to the centre, then back OUT in reverse order.

The composer is the foundational primitive behind modern web frameworks: routing, auth, logging, error handling, body parsing, and rate limiting are all expressed as middlewares that plug into the same shape. Get this one function right and you have the kernel of a framework.

Signature

type Middleware<Ctx> = (context: Ctx, next: () => Promise<void>) => Promise<void> | void;

function compose<Ctx>(
  middlewares: Array<Middleware<Ctx>>
): (context: Ctx, next?: () => Promise<void>) => Promise<void>;

Examples

Basic three-middleware run, showing the onion order:

const ctx = { log: [] };
const mw = (name) => async (c, next) => {
  c.log.push(`${name} before`);
  await next();
  c.log.push(`${name} after`);
};

await compose([mw('A'), mw('B'), mw('C')])(ctx);
// ctx.log === ['A before', 'B before', 'C before',
//              'C after',  'B after',  'A after']

Conditional descent — a middleware decides whether to call next():

const auth = async (c, next) => {
  if (!c.user) return; // short-circuit: downstream never runs
  await next();
};
await compose([auth, async (c) => { c.log.push('protected'); }])({ user: null, log: [] });
// log is [] — auth gated the chain

Top-level error handler via try/catch around await next():

const ctx = { log: [] };
await compose([
  async (_c, next) => {
    try { await next(); } catch (err) { ctx.log.push(`caught: ${err.message}`); }
  },
  async () => { throw new Error('boom'); },
])(ctx);
// ctx.log === ['caught: boom'] — composed promise resolved, not rejected

Calling next() twice in the same middleware is a hard error:

await compose([
  async (_c, next) => { await next(); await next(); },
  async () => {},
])({});
// rejects: Error('next() called multiple times')

Notes

  • Single-next constraint — within one invocation, each middleware may call its next() at most once. A second call rejects the composed promise with Error('next() called multiple times'). This catches the common bug where a middleware accidentally falls through to next() twice (e.g. after an error-handling branch that forgets to return).
  • Errors propagate through await next() — a sync throw or async rejection anywhere downstream surfaces in the awaiting parent. Wrap await next() in try/catch to handle it. If no middleware catches the error, it rejects the composed promise.
  • Onion order is strict[A, B, C] runs A-before, B-before, C-before, C-after, B-after, A-after. Skip next() and the chain stops at that layer; upstream parents that already awaited still resume normally because the skipping middleware simply returned.
  • Context is shared by reference — every middleware sees the same context object. Mutations are visible to siblings. Treat it like Koa's ctx.
  • Nesting works — the composed function is itself a valid middleware, so compose([outer, compose([inner1, inner2])]) is fine. The outer chain's next becomes the inner composed function's outerNext argument.
  • Async is optional — middlewares may be sync (return void) or async. Sync throws become rejections automatically.
  • No HTTP semanticscontext is just a plain object. Tests use { log: [] } so middlewares can push to it.
  • Validate inputscompose should throw a TypeError synchronously if middlewares is not an array or any element is not a function.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium