Discover.
Code 28 Jul 2026 3 min read

TypeScript at the Edge: Typing Your Whole Stack

From API routes to database rows to UI state — a single type system across the entire stack kills whole classes of bugs. Here's the setup that actually works without drowning you in boilerplate.

Sten

Senior Editor

TypeScript at the Edge: Typing Your Whole Stack

The most expensive bug in web development is the one that lives in the gap between two systems: the API returns a field renamed by a junior dev, the database column gets a new nullable constraint, the frontend ships a stale type. TypeScript can’t fix that — unless you stop typing each layer in isolation and start typing the contracts between them.

The one type that rules them all

Stop hand-writing API types. Define the schema once, in one place, and derive everything else from it:

import { z } from 'zod';

export const Article = z.object({
  id: z.string().uuid(),
  title: z.string().min(1).max(120),
  pubDate: z.coerce.date(),
  tags: z.array(z.string()).max(4),
  featured: z.boolean().default(false),
});

export type Article = z.infer<typeof Article>;

This one schema now drives:

  • Validation — every request body, every DB write, every file parsed.
  • Typesz.infer gives you the type for free; no drift between the runtime check and the compile-time type.
  • OpenAPI docszod-to-openapi generates the spec, so the API documentation can never rot.

The rule: types are derived, never duplicated. Every time you write an interface that mirrors a schema, a bug is born.

Edge to edge: the full pipeline

Here’s the pipeline that has held up across several production apps:

  1. DB layer: Drizzle (or Prisma) generates types from the migration schema. This is the source of truth for persistence.
  2. Contract layer: Zod schemas for everything crossing a boundary — API routes, queue messages, webhook payloads, env vars. z.enum for the values you’d otherwise typo as strings.
  3. Transport layer: tRPC or a typed RPC — when both sides share the schema package, calling a function on the server from the client is type-safe end to end. No more fetch + as MyType + prayer.
  4. UI layer: React Query (or similar) infers the response type from the query definition. If the server schema changes, the client breaks at compile time, not in production at 2am.

Validating the edges your types can’t see

Types check your code — they don’t check reality. Data arrives from the outside world, and it lies:

  • Never trust a parsed JSON blob. Validate it at the boundary with the schema, then convert to the typed value once. Validating once at the edge beats scattering if (x !== undefined) checks through the app.
  • Env vars are runtime data. Parse process.env through a schema at startup and fail fast — a missing DATABASE_URL should crash the deploy, not the app at 3am.
  • API responses age. Pin the API version in the URL and run a contract test against the live endpoint. Types catch drift at build time; contract tests catch it when the other team redeploys.

The payoff, measured

A stack like this is not free — it’s maybe 10% more upfront code for the schemas. What you get back:

  • Compile-time discovery of breaking API changes, instead of runtime errors.
  • One place to change when a field renames. npm run typecheck finds every usage.
  • Self-documenting code — the schema is the documentation, and it can’t go stale.
  • AI-assisted development gets safer — when your editor (or your agent) can see the exact contract, generated code stops guessing field names.

Start today, even on a legacy codebase

You don’t need a rewrite. Pick the worst boundary — usually the API client — and wrap it in a schema. Delete the hand-written interface, derive it from z.infer, and watch the diff noise disappear. Do the same for the next boundary, and the next.

A year from now, “the types were wrong” won’t be a thing your team says anymore. That’s the whole point.

Written by

Sten

Senior Editor

Builds AgenticOS and runs a homelab full of containers, GPUs and experiments.