Skip to content

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.

Engineering1 min read
One type carried from the database through the API to a button.

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.
ts
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.