Most bugs we ship are not logic errors. They are shape errors — a field that was optional here and required there, a number that arrived as a string, a rename that updated four call sites and missed the fifth. The fix is not more tests. It is making the wrong shape impossible to write down.
One source of truth
The schema is the root. Everything downstream is a projection of it, and every projection is derived — never hand-maintained in parallel. When the column changes, the type changes, and every consumer that no longer type-checks lights up in the editor.
model Article {
id String @id @default(cuid())
slug String @unique
title String
publishedAt DateTime?
author Author @relation(fields: [authorId], references: [id])
authorId String
}The generated client gives us a row type for free. We never redeclare it — we import type { Article } and let inference carry it the rest of the way.
The three seams
There are exactly three places a shape can drift. Name them, and you know where to put the guarantee:
- Database → server: the query result. Owned by the generated client.
- Server → client: the serialised payload. Owned by a shared types package.
- Client → screen: the component props. Owned by the same shared type.
Deriving, not duplicating
A card rarely needs the whole record. That is fine — derive a narrower view with a utility type, so it stays tied to the source:
type CardView = Pick<Article, "slug" | "title" | "excerpt">;
export function ArticleCard({ article }: { article: CardView }) {
return <a href={`/engineering-journal/${article.slug}`}>{article.title}</a>;
}The best type is the one you never wrote. Derive it, and a rename in the schema walks all the way to the button on its own.
What it buys you
| Without end-to-end types | With them |
|---|---|
| Rename ripples through PRs and QA | Rename fails the build at every stale site |
| API drift found in production | Drift found in the editor |
| Docs describe the shape | The type is the docs |
For the full pattern applied to a real build, see our case studies — the same thread runs through every one.
- #TypeScript
- #Prisma
- #Server Components
- #Developer Experience