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.

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
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
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 typed1/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
Quick check
In tRPC, how does the React app know the shape of the server's responses?
Go deeper
- 1Explore tRPC's approach to end-to-end type safety without manual API documentation.
- 2Compare it to generating a typed client from an OpenAPI/Swagger spec.
- 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.