TypeDrop

2026-09-18 Challenge

2026-09-18 Medium

Typed Middleware Pipeline with Context Propagation

You're building the request-processing core for a lightweight HTTP framework. Incoming requests flow through a chain of strongly-typed middleware functions — authentication, logging, rate-limiting, body parsing — each of which may enrich a shared context object before passing control to the next handler. The compiler must enforce that every middleware only reads context properties that have already been added by a preceding middleware, and that the final handler receives the fully-enriched context.

Goals

  • Define `Middleware<TIn, TOut>` and `Pipeline<TCtx>` so that each `.use()` call threads the accumulated context type `TCtx & TOut` to the next step.
  • Implement `createPipeline` as a fluent builder that executes middleware in order, passing the enriched context through a `next` callback chain.
  • Implement all four concrete middleware factories (`withRequestId`, `withAuth`, `withTimestamp`, `withParsedBody`), including the compile-time constraint that `withParsedBody` requires `rawBody: string` on the incoming context.
  • Implement the `AuthError` class with a `readonly code` of literal type `"UNAUTHORIZED"` and ensure it is thrown (not returned) by `withAuth` on an empty token.
challenge.ts
// Core types you must define:

export type Middleware<TIn, TOut> = (
  ctx: TIn,
  next: (enriched: TIn & TOut) => Promise<void>
) => Promise<void>;

export type Pipeline<TCtx> = {
  use<TOut>(mw: Middleware<TCtx, TOut>): Pipeline<TCtx & TOut>;
  run(handler: (ctx: TCtx) => Promise<void>): Promise<void>;
};

// Entry point:
export function createPipeline<TCtx extends object>(
  initialCtx: TCtx
): Pipeline<TCtx>;

// Example usage the compiler must accept:
createPipeline({ rawBody: '{"x":1}' })
  .use(withRequestId("api"))        // ctx gains requestId
  .use(withAuth("admin_token"))     // ctx gains userId, role
  .use(withTimestamp())             // ctx gains timestamp
  .use(withParsedBody(JSON.parse))  // ctx gains body — rawBody already present ✅
  .run(async (ctx) => {
    console.log(ctx.requestId, ctx.userId, ctx.role, ctx.timestamp, ctx.body);
  });
Hints (click to reveal)

Hints

  • The key to type-threading is making `.use<TOut>()` return `Pipeline<TCtx & TOut>` — TypeScript will intersect the types at each call site automatically.
  • Model the middleware chain internally as an array of functions and compose them right-to-left (or use a recursive `compose`) so each `next` call advances to the following middleware.
  • For `withParsedBody`, the constraint `TIn extends { rawBody: string }` lives on the *function's* type parameter, not on `Middleware` itself — this is what lets the compiler reject its use before `rawBody` is present.

Or clone locally

git clone -b challenge/2026-09-18 https://github.com/niltonheck/typedrop.git