TypeDrop

2026-07-21 Challenge

2026-07-21 Hard

Typed Middleware Pipeline with Branching & Typed Context

You're building the request-processing core for an API gateway. Incoming requests flow through a chain of middleware functions that can read and write a strictly-typed context object, short-circuit the pipeline by returning an early response, or pass control to the next middleware — all while preserving full type safety across every stage of the chain.

Goals

  • Implement `createPipeline` so that middleware registered via `.use()` runs in order, each receiving the context enriched by all previous patches, and the chain short-circuits on the first returned `PipelineResponse`.
  • Ensure `.use()` correctly widens the pipeline's context type using `Merge<TCtx, TPatch>` so downstream middleware see all previously added fields at compile time.
  • Implement the three pre-built middleware (`withLogging`, `withAuth`, `withRateLimit`) so they compile without errors against their declared `TCtx` constraints and pass all five test assertions.
  • Enforce at the type level that `withAuth` can only be placed after a context containing `token: string`, and `withRateLimit` only after a context containing `userId: string` — incorrect ordering must produce a compile error.
challenge.ts
// Core types + main builder signature

export type OkResponse    = { readonly kind: "ok";    readonly status: number; readonly body: unknown };
export type ErrorResponse = { readonly kind: "error"; readonly status: number; readonly message: string };
export type PipelineResponse = OkResponse | ErrorResponse;

/** Merges two object shapes, with B overriding A on key conflicts. */
export type Merge<A, B> = Omit<A, keyof B> & B;

export type NextFn<TCtx, TPatch extends object> =
  (patch: TPatch) => Promise<PipelineResponse>;

export type MiddlewareFn<TCtx, TPatch extends object> =
  (ctx: TCtx, next: NextFn<TCtx, TPatch>) => Promise<PipelineResponse>;

/** Fluent builder — each .use() widens the context type. */
export interface Pipeline<TCtx> {
  use<TPatch extends object>(
    fn: MiddlewareFn<TCtx, TPatch>
  ): Pipeline<Merge<TCtx, TPatch>>;
  run(ctx: TCtx): Promise<PipelineResponse>;
}

export function createPipeline<TCtx>(): Pipeline<TCtx> { /* TODO */ }
Hints (click to reveal)

Hints

  • The key to `.use()` returning a richer `Pipeline` is to store middleware as an array of `(ctx: unknown, next: ...) => Promise<PipelineResponse>` internally and cast only at the boundary — but avoid `any` by using a private generic accumulator class that satisfies the `Pipeline<TCtx>` interface.
  • To chain middleware in `run()`, build the handler chain from right to left (like Koa/Express): start with the default 200 handler, then wrap each middleware around it in reverse order, passing the merged context forward via the `patch` argument.
  • For `withLogging`, check whether `(ctx as Record<string, unknown>).log` is already an array using `Array.isArray` — this lets you append to an existing log without using `any` or type assertions on the patch.

Or clone locally

git clone -b challenge/2026-07-21 https://github.com/niltonheck/typedrop.git