Stop 07 · TypeScript Across the StackTopics · 5/7

Typed API Contracts

Part of the Full Stack Developer Roadmap

In short

Shared types still let the frontend call a wrong URL or send a wrong body. A typed API contract ties every call to its server route, so backend changes break the frontend build, not production.

Going a step further than shared types — tools like tRPC or a typed OpenAPI client generate full, type-safe API calls so a backend change breaks your frontend build immediately instead of silently at runtime.

The mascot sits between a tenant and owner holding identical sealed papers and shaking hands.

Think of it like this

A signed rent agreement

When you rent a flat, both you and the owner sign the same agreement and keep identical copies. Neither side can quietly change the rent. If terms change, a new agreement is signed and both copies update. A typed API contract works the same way.

Watch it happen

A backend rename, caught before deploy

API change
Contract
Frontend
Type check
CI blocks

1/5A backend dev renames the user's name field to fullName in the userById procedure.

The big ideas

What you need to know

tRPC for TypeScript monorepos

The frontend imports only the router's type and calls procedures like functions, with input and output fully typed.

OpenAPI for any client

Describe the API in an OpenAPI spec, then generate TypeScript types or clients with tools like openapi-typescript.

Validate at the boundary

Contracts check your code at build time. Zod input schemas still check real requests at runtime.

Run tsc in CI

The safety only works if every pull request runs a full type check across both apps.

See it in code

Walk through the code

ts
1// server/router.ts2import { initTRPC } from "@trpc/server";3import { z } from "zod";4const t = initTRPC.create();5export const appRouter = t.router({6  userById: t.procedure7    .input(z.object({ id: z.string() }))8    .query(({ input }) => db.user.findUniqueOrThrow({ where: { id: input.id } })),9});10export type AppRouter = typeof appRouter;11 12// client/trpc.ts (imports only the TYPE, no server code)13import { createTRPCClient, httpBatchLink } from "@trpc/client";14import type { AppRouter } from "../server/router";15const trpc = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: "/api/trpc" })] });16const user = await trpc.userById.query({ id: "u_42" }); // input and output fully typed

1/4One procedure with a Zod input. Its return type comes straight from the database call.

This is tRPC v11. Calling userById with a number id, or reading a field that doesn't exist, fails to compile.

Save yourself the pain

Common mistakes

Oops

Writing fetch("/api/user/" + id) by hand next to a typed client and expecting it to be checked.

Do this instead

Route every call through the typed client, or the contract can't protect that call.

Oops

Generating an OpenAPI client once and never regenerating it.

Do this instead

Regenerate in a script that runs in CI, so the client always matches the current spec.

Your mission

Break the build on purpose

~40 min

Quick check

In tRPC, how does the React app know the shape of the server's responses?

Go deeper

  1. 1Explore tRPC's approach to end-to-end type safety without manual API documentation.
  2. 2Compare it to generating a typed client from an OpenAPI/Swagger spec.
  3. 3Understand the core value: catching frontend/backend mismatches at compile time, not in production.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge