Build a typed API in 10 minutes with tRPC — no codegen, no schemas

1 month ago

This is The Manual. One library, its own documentation, one sitting. Today, T R P C. By the end you will have a typed A P I running, and every step comes straight from the official docs, which stay on screen the whole time.

Ask

Ask about this presentation

Answers are generated from this presentation.

Chapters

  1. 0:00Build a typed API in 10 minutes
  2. 0:15What you are building
  3. 0:4601 · Install
  4. 1:0702 · Initialize once
  5. 1:3303 · A validated procedure
  6. 2:0004 · Serve it
  7. 2:1805 · The typed client
  8. 2:45Gotchas
  9. 3:20The snippet ships
Show transcript

Build a typed API in 10 minutes

The Manual · tRPC · series opener
Build a typed API in 10 minutes with tRPC
No codegen, no shared schemas. Taught from the official documentation at trpc.io, which stays on screen the whole time.
the manual · bisque.todayep 01

This is The Manual. One library, its own documentation, one sitting. Today, T R P C. By the end you will have a typed A P I running, and every step comes straight from the official docs, which stay on screen the whole time.

What you are building

The idea · five steps
Your server's types become your client's types
One TypeScript type travels from server to client. The docs ask for TypeScript 5.7 or newer and strongly recommend strict mode.
01Install
02Router
03Procedure
04Serve
05Typed client
the manual · bisque.today02

Here is why T R P C is shaped the way it is. Most stacks keep the A P I contract somewhere in the middle, a schema file, generated code, something you have to regenerate. T R P C deletes the middle. Your server exports one TypeScript type, your client imports it, and the compiler does the rest. That is also why the docs ask for TypeScript five point seven or newer, with strict mode on. The types are the product. Five steps. Install, router, procedure, serve, and the typed client. Let's go.

01 · Install

01 · install
Two packages, one minute
trpc.io › docs › quickstart

"npm install @trpc/server @trpc/client"

The server package defines the API. The client package calls it. We add zod now because step three validates inputs with it.

The docs run npm. We run bun. Same packages, same result.
the manual · bisque.today03

Step one. The quickstart asks for two packages, the server package and the client package. The docs run n p m. We run bun, and we add zod while we are here, because step three will validate inputs with it. Watch the install land. That is the whole footprint. No code generator, no build step, nothing to keep in sync.

02 · Initialize once

02 · the router
Initialize tRPC exactly once
trpc.io › docs › server › routers

Define Routers

"You should initialize tRPC exactly once per application. Multiple instances of tRPC will cause issues."

"You'll notice we are exporting certain methods of the t variable here rather than t itself."

server/trpc.ts
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;
the manual · bisque.today04

Step two, and the first rule the manual states outright. You initialize T R P C exactly once per application. More than one instance causes real trouble, so this file exists to be imported everywhere else. Create the instance. Then export just the pieces you will use, the router helper and a public procedure. The docs are explicit about why. You export methods of t, never t itself, so the whole codebase speaks one idiom.

03 · A validated procedure

03 · the procedure
Validate the input, export the type
trpc.io › docs › quickstart

Procedures take an input parser — here a zod schema — and tRPC validates every call against it before your code runs.

the load-bearing line: export the TYPE, not the router
server/appRouter.ts · trimmed to fit — full file in the episode notes
import { publicProcedure, router } from './trpc';
import { z } from 'zod';

export const appRouter = router({
  userById: publicProcedure
    .input(z.string())
    .query(async (opts) => {
      const { input } = opts;
      return { id: input, name: 'Katt' };
    }),
});

export type AppRouter = typeof appRouter;
the manual · bisque.today05

Step three is where the shape pays off. A procedure named user by I D takes an input parser, a zod schema, and T R P C validates every call before your code runs. The query gets the parsed input, already typed as a string, and returns a user. And then the load bearing line of the entire library. You export the type of the router. The type, not the router. The client will import only this, so no server code ever leaks into the browser bundle.

04 · Serve it

04 · serve
Three lines and it's listening
server/index.ts
import { createHTTPServer }
  from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';

const server = createHTTPServer({
  router: appRouter,
});

server.listen(3000);
the manual · bisque.today06

Step four. The standalone adapter wraps your router in a plain node H T T P server. Listen on a port. That is genuinely the whole server. Adapters exist for express, fastify, next, and the edge runtimes, and the router code never changes. Run it, and the first request streams back.

05 · The typed client

05 · the payoff
The client knows your API already
trpc.io › docs › quickstart

"You can also use your autocomplete to explore the API on your client" — type trpc.u and every procedure surfaces.

Full typesafety "without leaking any implementation details to the client."

client/index.ts
import { createTRPCClient, httpBatchLink }
  from '@trpc/client';
import type { AppRouter } from '../server/appRouter';

const trpc = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: 'http://localhost:3000' })],
});

const user = await trpc.userById.query('1');
//    ^ typed: { id: string; name: string }
the manual · bisque.today07

Step five, the payoff. Import the type. Note the word type in that import, it means the server code itself never ships to the browser. Hand it to the client constructor with a batching link. And now the call site is fully typed. The input must be a string, the result is a user object, and if the server renames a field, this line turns red before you ever run it. The docs put it plainly. Type t r p c dot u, and your whole A P I surfaces in autocomplete.

Gotchas

Gotchas · from the real issue tracker
Three walls people actually hit
1Big routers break declaration builds.Past a few hundred procedures TypeScript refuses to serialize the inferred type (error TS7056). Annotate the router type or split libraries.trpc/trpc #6003
Your Dates arrive as strings.JSON has no Date. Wire a data transformer like superjson on BOTH server and client, or every timestamp quietly becomes a string.2trpc/trpc #4515
3Loose tsconfig quietly kills inference.Without strict mode the types degrade instead of erroring. The docs' strict-mode note exists because people hit this.trpc/trpc #1506
the manual · bisque.today08

Before you ship this, three walls people actually hit, straight from the issue tracker. One. Very large routers can break declaration builds. TypeScript refuses to serialize the inferred type once it gets enormous. Annotate the router type or split your libraries. Two. Your dates arrive as strings, because JSON has no date type. Wire a transformer like superjson on both the server and the client, or every timestamp quietly degrades. Three. A loose T S config quietly kills inference. Strict mode is not a style preference here, it is load bearing.

The snippet ships

The Manual · always ships a snippet
Three files. Typed end to end.
the whole API · server/trpc.ts · server/appRouter.ts · client/index.ts
// server/trpc.ts     initialize once, export helpers
// server/appRouter.ts a validated procedure + export type AppRouter
// client/index.ts     createTRPCClient<AppRouter> + typed calls

const user = await trpc.userById.query('1');
const made = await trpc.userCreate.mutate({ name: 'Katt' });

Every line in this episode came from trpc.io/docs. The full runnable snippet ships with the episode.

Next in the series: context and middleware — where auth lives.
the manual · bisque.today09

That is the whole A P I. Three small files, one exported type, and a client that refuses to compile when the server changes underneath it. The full runnable snippet ships with this episode, so you can paste it and go. Next in the series, context and middleware, which is where auth lives. The Manual. Read so you don't have to wander.