03Note
Typed from the database to the button
Why we carry types across every layer of a product, and the few decisions that make it cheap.
The most expensive bugs we see live between layers: a field renamed in the database, a response shape changed in the API, a screen that still expects the old one. Types that stop at the network boundary cannot catch them.
One source of truth#
We derive types from the schema, never write them twice. The database schema generates the query types, the API contract is inferred from the handlers, and the client imports it. Rename a column and the compiler walks you to every screen that used it.
In practice that means three rules:
- The database schema is the only place a field is declared.
- API types are inferred from the handlers, never written by hand.
- The client imports those types; it never redeclares them.
const post = await api.posts.get({ id });// ^? { id: string; title: string; publishedAt: Date }Validate at the edges#
Types describe what should arrive; validation checks what did. We use Zod for this, so z.infer gives the type for free. Every input from outside the system, from forms to webhooks, passes through a schema before it touches business logic. The same schema produces the type, so the two never drift.
None of this is exotic. It is a handful of decisions made early, and it is the reason our teams can refactor on a Thursday afternoon without holding their breath.