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.
Useful resources
Or clone locally
git clone -b challenge/2026-09-18 https://github.com/niltonheck/typedrop.git