TypeDrop

2026-03-04 Challenge

2026-03-04 Hard

Typed Plugin Middleware Chain with Typed Error Hierarchy

You're building the request-processing core of an API gateway. Incoming requests pass through a chain of strongly-typed middleware plugins (auth, rate-limiting, transformation, logging). Each plugin can either pass the request downstream, short-circuit with a typed error, or mutate the request context — and the orchestrator must collect per-plugin results, surface a typed error hierarchy, and guarantee exhaustive handling at every exit point.

Goals

  • Define the full typed error hierarchy (AuthError, RateLimitError, ValidationError, UpstreamError) as a discriminated union and wire it through Result<T,E> and Plugin.
  • Implement runChain to execute plugins sequentially, short-circuit on the first failure, record accurate durationMs per plugin, and mark unexecuted plugins as 'skipped'.
  • Implement renderError with an exhaustive switch over all PluginError variants, using a never-assertion in the default branch to guarantee compile-time completeness.
  • Implement summariseAudit to aggregate a PluginRecord[] into an AuditSummary — narrowing each record variant to compute counts, total duration, and per-plugin error details.
challenge.ts
// Key types and main function signature

export type RequestId  = string & { readonly __brand: "RequestId" };
export type PluginName = string & { readonly __brand: "PluginName" };

export type PluginError =
  | { kind: "auth";       reason: string }
  | { kind: "rate_limit"; retryAfterMs: number }
  | { kind: "validation"; field: string; issue: string }
  | { kind: "upstream";   statusCode: number; body: string };

export type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

export interface Plugin {
  name: PluginName;
  execute(ctx: RequestContext): Promise<Result<RequestContext, PluginError>>;
}

export type ChainResult =
  | { outcome: "success"; finalContext: RequestContext; audit: PluginRecord[] }
  | { outcome: "failure"; failedPlugin: PluginName; error: PluginError; audit: PluginRecord[] };

export async function runChain(
  plugins: Plugin[],
  initialContext: RequestContext
): Promise<ChainResult> { /* TODO */ }
Hints (click to reveal)

Hints

  • A `never`-assertion helper like `function assertNever(x: never): never { throw new Error(...) }` makes the default branch of your switch exhaustive at compile time.
  • For PluginRecord, keep `durationMs` only on the `passed` and `failed` variants — the `skipped` variant has no timing to record, which naturally guides your summariseAudit accumulator.
  • In runChain, capture `Date.now()` before and after each `await plugin.execute(ctx)` call; store the delta as `durationMs` before deciding which PluginRecord variant to push.

Or clone locally

git clone -b challenge/2026-03-04 https://github.com/niltonheck/typedrop.git