I Built a Type-Safe API in a Weekend. Here's What Actually Mattered.

I keep seeing tutorials that frame type-safe APIs as a stack choice. After shipping three of them, I think the stack is mostly noise — and a handful of unglamorous decisions do almost all the work.

AU

Admin User in Technology

April 25, 2026 · 7 min read

I Built a Type-Safe API in a Weekend. Here's What Actually Mattered.

Every six months a new "type-safe full-stack" framework arrives and the discourse resets. tRPC, Hono RPC, oRPC, GraphQL Codegen, the latest end-to-end whatever. The pattern is always the same: you'll lose a weekend wiring it up, you'll feel briefly invincible, and three months later you'll be writing the same kinds of bugs you wrote before.

I've now shipped three type-safe APIs at production scale. None of them were undermined by the framework I chose. All of them were saved or sunk by the same small set of decisions — none of which appear in a stack diagram.

Validation at the boundary, full stop

TypeScript types are a fiction the compiler tells itself. They evaporate at runtime. If you don't validate inputs at the edge — request body, query params, environment variables — your "type-safe" API is just one bad JSON.parse from a 500.

Pick one schema library — Zod, Valibot, ArkType, doesn't matter — and use it everywhere a request enters or leaves your system. Stop arguing about which one. The difference between them is rounding error compared to the difference between "we have schemas" and "we don't."

Share types, don't generate them

A monorepo with a shared package beats codegen every time. Codegen has a build step. Build steps drift. Drift becomes "why are my types wrong on Friday afternoon."

If your API and client live in the same repo — they probably should — export the route types directly and import them in the client. No watcher, no pipeline, no cache to bust. The compiler is your build step.

Code generation is a bet that your tools won't break. Direct type imports are a bet that the language will keep working. I know which bet I'd rather take.

Errors are part of the type, not an exception

The biggest unforced error in API design is treating failure as something that happens outside the type system. Once you write your first try/catch around a fetch that returns "the right shape or maybe an Error," you've already lost.

Model your responses as discriminated unions. Make the client deal with the failure case at the type level. It feels like more code on day one. It is dramatically less code by month three.

What I'd actually pick today

Hono on the server, Zod for schemas, a thin shared package for types and validators, and a tiny fetch wrapper on the client. No tRPC, no GraphQL, no codegen. Boring on purpose. It will still be boring in two years, which is the entire point.

💬 0

Comments (0)

Comments (0)

Sign in to join the conversation.