Skip to content

DR-137 — A route's wire contract is enforced on the payload, never on the return type

DR-137 — A route’s wire contract is enforced on the payload, never on the return type

Section titled “DR-137 — A route’s wire contract is enforced on the payload, never on the return type”

Every /api/okf/* handler declared its return type as Promise<NextResponse<OkfBundleEnvelope> | NextResponse>. The union’s second arm exists for the error paths, which return { error: string } bodies.

That second arm is NextResponse<unknown>, and it accepts any body. The annotation was therefore decorative: it read as a contract and enforced nothing. Isolated probe, S544:

// with the union arm — NO ERROR
async function p1(): Promise<NextResponse<OkfBundleEnvelope> | NextResponse> {
return NextResponse.json({ totally: 'wrong', nope: 42 });
}
// without it — errors
async function p2(): Promise<NextResponse<OkfBundleEnvelope>> {
return NextResponse.json({ totally: 'wrong', nope: 42 }); // TS2322
}

This matters here because the OKF graph has two parallel type families: the server builds the payload with BundleGraphNode etc. (lib/okf/bundle-graph.ts, server-only — it imports node:fs), and the client reads it as OkfBundleGraphNode etc. (lib/query/okf.ts, hand-written mirrors). They are meant to be the same shape and nothing compared them.

Mutation, S544: make the server emit size as a string and add a required field the client has never heard of. Result before the fix — bun run typecheck clean, 242 OKF tests pass. A second mutation renaming BundleNavConcept.path was caught only by that module’s own unit test; all six API-route test files passed, so there was no runtime guard at the boundary either.

The families did in fact still agree. The system was unguarded, not broken — which is exactly the state that produces a silent break later.

A route that returns a typed body annotates the success payload at its own declaration:

const payload: OkfBundleEnvelope = { ...graph, nav, log };
return NextResponse.json(payload);

The | NextResponse arm stays for the error paths. Where a server-side builder and a client-side wire type are separate declarations of one shape, this annotation is the contract between them, and every route serving such a shape must carry it.

Drop the | NextResponse arm and type every error body. Would enforce the contract at the return type, but forces an error-envelope union through every handler for no gain over annotating one line.

Share one declaration between server and client. The obvious fix, and not available: bundle-graph.ts imports node:fs and cannot enter a client bundle. The mirror is deliberate; only the checking was missing.

Runtime validation (Zod) at the boundary. Rejected as the primary mechanism: both sides are our own code, so the failure is a compile-time drift, not untrusted input (see DR-138).

  • With the annotations in place, both S544 mutations fail loudly and by name at graph/route.ts and union-graph/route.ts.
  • Annotating resource/route.ts’s two payloads gave OkfResourceRecordResult and OkfResourceListResult real cross-file consumers, clearing their knip findings without a suppression — the two decisions reinforce each other (DR-136).
  • Any new /api route serving a lib/query/* wire type inherits this requirement. The pattern is not OKF-specific; the same NextResponse<T> | NextResponse shape appears across the API surface and has the same non-enforcement wherever it does.
  • Not retrofitted beyond /api/okf/* in S544. Other routes carrying the shape are unaudited.