How do types go from the database to the client?
The bug arrives as a polite 500. Support pastes the ticket. Engineering opens the settings screen. Everything looks fine locally. Production data has a nullable field the UI never handled because someone hand wrote the API type six months ago and nobody updated it when the migration landed.
The postmortem will call it a communication gap. That is corporate for "we lied to ourselves about where truth lives." The fix is structural: one typed path from the database to the client so drift becomes a build failure instead of a customer surprise.
My default: Postgres owns the shape. Application code reads through a typed layer. HTTP boundaries export what clients may see. UI imports those shapes instead of inventing parallel ones. When any station rewrites the luggage tags by hand, the assembly line lies.
Four stations, one spine
Picture data moving through four stations.
- Postgres tables define what exists and what constraints apply.
- Application code reads and writes through a typed data layer.
- HTTP or RPC boundaries expose shapes clients can call.
- UI code renders fields it expects and handles errors it understands.
PostgreSQL is the durable memory. Drizzle is a common TypeScript first way to declare tables, generate migrations, and infer query result types. The client might be HTMX swapping HTML, a React app, or a mobile shell. The principle does not care about the renderer. It cares that nobody invents a parallel schema in the front end.
Station one: schema as source of truth
Start where the data lives. Tables, columns, nullability, enums, and foreign keys are product decisions encoded in SQL. When those decisions live only in a diagram or one senior engineer's head, every downstream type is guesswork.
Schema first workflows check definitions into Git. Migrations apply in order. Preview databases on hosts like Neon exercise migrations before merge. Drizzle infers TypeScript types from the same definitions that emit SQL. The type story begins here.
| Database fact | TypeScript effect |
|---|---|
email is text not null unique |
Inserts require a string; duplicates fail at the database |
status is an enum |
Application code gets a union of allowed strings |
Relation to orders |
Queries can join with typed nested objects |
When the schema changes, the migration and the TypeScript definitions change together in one pull request. Reviewers see product impact and type impact at once.
Station two: application layer honesty
Server code should not cast query results to any at the door. Drizzle queries return types tied to table definitions. Hand written SQL can still be typed, but those interfaces must update when columns change.
This layer is where business rules live that Postgres cannot express cleanly. A column might be nullable because legacy rows exist, but new API responses might guarantee a default. Document that choice in the function that shapes the response, not in a comment buried in the UI.
Patterns that work for small teams:
One module owns data access. Routes call service functions. Service functions call Drizzle. UI routes do not embed raw SQL strings.
Map database rows to API DTOs on purpose. Sometimes the client should not see internal columns. Explicit mapping makes omissions visible in review instead of accidental leaks through object spreads.
Test migrations against typed queries. CI applies migrations to a throwaway database, then runs tests that exercise the typed accessors. Types catch renames. Tests catch logic.
Station three: the boundary clients call
Clients never talk to Postgres directly in a well behaved product. They talk to HTTP, WebSockets, or RPC. That boundary is where untyped stacks break down. Someone generates OpenAPI from nowhere, or worse, front end developers read network tabs and invent interfaces.
Better options:
Shared package. Monorepos export types from the server module clients import. Change propagates instantly. Build fails if versions mismatch.
Schema generated OpenAPI. Tools derive HTTP types from route definitions. The generation step must run in CI so drift fails builds.
Server driven UI with typed partials. HTMX approaches still benefit from typed view models on the server even when the browser receives HTML instead of JSON.
How do you connect a simple front end to a typed API? covers the front edge. Same theme: pick the contract before debating widget libraries.
Station four: client consumption without fantasy types
The UI should import or generate types from the boundary, not recreate them. When designers add a field to a settings screen, the workflow is: migration, server type update, client type update, component change. Four steps linked, not three steps plus hope.
For React teams, props mirror API DTOs. For HTMX teams, server templates receive typed view models. For mobile, codegen from OpenAPI produces Swift or Kotlin structs. The mechanism varies. The rule does not.
Client code still validates at runtime for untrusted input. Types do not replace sanitization on user generated content. They do replace guessing about your own API responses.
Where the flow breaks
Knowing the ideal path helps you spot shortcuts that will tax you later.
Parallel type files maintained by hand. User.ts in the front end and User.ts in the back end diverge quietly until launch week.
Optional everything. TypeScript allows ? on every field when requirements are fuzzy. The compiler stays green while the product becomes impossible to reason about.
Skipping CI on type checks for speed. Preview deploys that ignore types teach the team that green builds lie.
Leaking raw database rows to clients. Even perfect types cannot fix exposing internal identifiers or soft deleted records because someone returned ORM entities directly.
Framework churn without boundary stability. Rewriting React hurts less when API types stayed stable. Rewriting everything at once is a company event, not a refactor.
Governance without a platform team
Small teams skip governance when it sounds enterprise. Typed vertical slices are governance you can afford.
Pull requests include schema, server, and client touch points together. Feature work that changes data shape should not merge as application only diffs.
Renames are product events. BI tools, exports, and integrations read column names. Types surface code impact. Humans still own customer comms.
One obvious path for new engineers. "Where do types come from?" should have one answer: follow the schema forward.
A minimal stack that respects the flow:
| Piece | Role |
|---|---|
| Postgres | Durable records and constraints |
| Drizzle | Schema, migrations, typed queries |
| Node or Bun | Application server |
| Typed routes | HTTP handlers with shared input/output types |
| Thin client | HTMX or React consuming the same shapes |
Preview branches apply migrations before merge. TypeScript runs in CI. Client bundles import types from the same commit hash as the server they call.
Lessons
Users experience type drift as broken screens, wrong totals, and forms that submit but do not save. Finance sees numbers that do not reconcile. Support hears "works on my machine" from engineering.
A typed path from database to client is how five people simulate the discipline larger companies buy headcount for. It is not bureaucracy. It is alignment between what the vault stores and what the screen promises.
When your stack does that, migrations stop feeling like roulette. UI work stops guessing field names. You stop hearing "we just missed that edge case" for problems the compiler could have flagged Tuesday.
What I would do again: schema first, typed boundaries, one pull request per data shape change.
What I would not do again: hand maintained duplicate type files in front and back end folders.
Work with Kleto
I am James Cowan, founder of Kleto. We wire Postgres, Drizzle, and typed API boundaries so your schema, server, and client stay one story as the product grows. Contact Kleto if that matches your stack.