ADR 0003 — Two context types: RuntimeContext and HandlerContext
- Status: Accepted
- Date: 2025-05
Context
A handler wants a typed context — ctx.params and ctx.input shaped by the
endpoint’s schemas — for a good developer experience. But the transport layer
assembles the context before those types are known: right after a Zod parse,
params and input are unknown.
An earlier framework used a single HandlerContext<TParams, TInput> everywhere,
including in the transport, and bridged the gap with as casts — one in the
transport, another in the handler-binding step.
Decision
Split the context into two interfaces.
RuntimeContext—{ params: unknown, input: unknown, … }. Used by the transport and by lifecycle hooks. No generics, no casts.HandlerContext<P, I>—{ params: P, input: I, … }. Used only inside handlers, where the types are known.
implement() is the bridge. It accepts typed Handlers<C> and wraps each one
as a MethodDef.handler(ctx: RuntimeContext). Runtime safety comes from the
Zod parse that runs before the handler; type safety comes from generics that
infer P and I from the endpoint schemas.
Consequences
- Zero
ascasts on the framework’s request path. - The transport works with loose
unknowntypes and never lies about them. - Handlers see a fully typed context; the cast that used to bridge the two worlds no longer exists — it was replaced by an honest type boundary.