Why one package describes the transport, and what it buys both sides.
What is in it
packages/contracts holds the schemas and the typed client:
- zod schemas per feature: request bodies, responses, query strings, path params
- a registry mapping an operation name to method, path, schemas and metadata
- type helpers derived from that registry — there is no hand-written interface
HttpClient, which calls operations by name, validates every response, and
refreshes an expired token
- the SSE reader, which yields validated stream events
- the version constant, and the OpenAPI generator built from the registry
One definition, three consumers
The same schema is used to:
- validate requests in the API (a validation pipe),
- validate responses in the browser (on arrival),
- generate the OpenAPI specification published as the reference.
A shape is therefore declared once. The failure this prevents is not exotic: a
backend that adds a required field, a frontend that has not been updated, and a
deploy where sign-in silently stops working.
Why zod rather than generated types
The type could be generated from the schema, but then it would be checked at
compile time only — and the interesting failures happen when the bytes arrive. A
schema that validates at runtime can be the single definition; the types are
inferred from it, not maintained beside it.
This is what makes the strictness rule affordable: application code may not use
any, precisely because the boundary is already typed and checked.
Why validation happens on the client too
An API that is correct is still not always reachable: a proxy can rewrite a body,
a cache can serve a stale response, two deployments can drift. Validating on
arrival turns those into an immediate, named failure at the boundary instead of
undefined surfacing three layers into a component.
The cost is one parse per response, which is not measurable next to the network
round trip it follows. See architecture.
The registry is the API’s inventory
operations lists every endpoint. Adding an entry there gives you, immediately:
- a typed client method
- the request and response types
- an entry in the OpenAPI specification
- a place to declare which error codes it can produce
The specification test asserts that the document and the registry agree, and that
every declared error code is documented — so an endpoint cannot be published with
an undocumented failure mode. See add an endpoint.
Versioning the contract
version.ts holds the version. Paths in the registry are version-relative and the
client prefixes them, so a version bump is a one-line change that moves the
client, the server and the document together. See
versioning.
What the contract deliberately does not contain
- Business rules. “A campaign cannot go from draft to sent” lives in the domain,
not in a schema.
- Persistence. A repository may store an id the contract never sees.
- Anything framework-specific. The API’s controllers and the browser’s components
both sit outside this package, which is why it can be shared at all.
Last modified on October 6, 2026