Understanding JSON → tRPC
End-to-end types, zero schema language.
What tRPC does that REST and GraphQL don't, and how a JSON sample scaffolds a router with input validation and inferred client types.
No schema, just TypeScript.
REST APIs have OpenAPI; GraphQL has SDL. tRPC has neither — the contract between client and server is the TypeScript types of the router itself. The client imports type AppRouter = typeof router and gets fully-typed calls: parameter types, return types, error types, all inferred. No code generation, no schema file, no runtime mismatch. The cost is that it only works when client and server are both TypeScript and can share types directly.
Procedures: query, mutation, subscription.
tRPC has three procedure types. query for reads (cached, idempotent); mutation for writes (not cached, invalidates); subscription for server-sent events. Each takes an optional input validator (typically a Zod schema) and returns whatever its handler returns. The shape of a router is a nested object literal whose leaves are procedures.
A worked router.
From a JSON sample { "id": 7, "email": "a@b.com", "name": "Q" } the codegen tool emits a scaffolded router: import { z } from "zod";
import { router, publicProcedure } from "./trpc";
export const userRouter = router({
list: publicProcedure.query(async () => { return []; }),
byId: publicProcedure
.input(z.object({ id: z.number().int().positive() }))
.query(async ({ input }) => { /* fetch by id */ }),
create: publicProcedure
.input(z.object({ email: z.string().email(), name: z.string().min(1) }))
.mutation(async ({ input }) => { /* insert */ }),
}); Three procedures: list, byId, create. Inputs are Zod schemas; client types are inferred from these. The handler bodies are the only thing left to write.
Three procedures
list + byId + create
Stock CRUD shape; the user fills in the bodies.
userRouter = router({ list, byId, create })
= Typed API
Zod is the runtime gate.
tRPC's .input(schema) uses any validator that conforms to its { parse } interface — Zod is by far the most common. The schema runs at runtime on every request; bad input rejects with a 400 before the handler runs. The TypeScript types are inferred from the Zod schema too, so the same definition gives you compile-time client types and runtime validation. That's the trick that makes tRPC work without a separate schema file.
Why JSON-to-tRPC is structural, not semantic.
A JSON sample tells you the entity's shape — what to create / read / update. It can't tell you the procedure granularity, the access control rules, the audit requirements, or the batching strategy. Codegen scaffolds the stock CRUD shape; most real routers diverge by either combining procedures (a single upsert rather than separate create and update) or specialising them (a listForOrganisation with auth checks).
When tRPC isn't the answer.
tRPC requires that client and server share TypeScript types. That works for a Next or Remix app where you control both ends. It doesn't work for a public API consumed by third parties, mobile apps not written in TypeScript, or anything that might one day need to be polyglot. For those, fall back to REST + OpenAPI or GraphQL. tRPC is the right choice precisely when the shared-types invariant holds.