Sachith Dassanayake Software Engineering tRPC for type‑safe end‑to‑end contracts — Cheat Sheet — Practical Guide (Dec 27, 2025)

tRPC for type‑safe end‑to‑end contracts — Cheat Sheet — Practical Guide (Dec 27, 2025)

tRPC for type‑safe end‑to‑end contracts — Cheat Sheet — Practical Guide (Dec 27, 2025)

tRPC for type‑safe end‑to‑end contracts — Cheat Sheet

Level: Intermediate

As of December 27, 2025 — focused primarily on tRPC versions 10.x and 11.x (stable)

Introduction

Type‑safe end‑to‑end contracts are increasingly vital for maintainability, developer productivity, and runtime reliability of modern web applications. tRPC is a pragmatic RPC framework that enables you to define backend APIs with full TypeScript type inference on clients — without code generation or schema duplication.

In this cheat sheet, we give you practical guidance on using tRPC (v10 and v11) to build type-safe APIs that keep client and server contracts in sync. We’ll cover prerequisites, hands-on steps, validation integration, common pitfalls, and a TL;DR checklist for quick reference.

Prerequisites

Before you dive into tRPC, ensure you have the following in place:

  • TypeScript (v4.9+): tRPC leverages TypeScript’s enhanced type inference and utility types.
  • Node.js (v18+ suggested): Supports modern JS features and ES module syntax where applicable.
  • JavaScript runtime framework: Next.js, Express, Fastify, or similar (tRPC is framework-agnostic but offers integrations).
  • tRPC libraries: `@trpc/server` and `@trpc/client` at stable versions 10.x or 11.x.
  • Optional but recommended: Zod (v3+) for schema validation, which integrates seamlessly.

If you are targeting experimental React Server Components or alternative platforms, check tRPC’s previews and community extensions, but for most applications, stable releases suffice.

Hands-on steps

1. Define your router and procedures

Create your root router by defining procedures that encapsulate business logic:


// src/server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() })) // input validation with Zod
    .query(({ input }) => {
      return { id: input.id, name: 'Alice' }; // example response
    }),
  createUser: t.procedure
    .input(z.object({ name: z.string().min(1) }))
    .mutation(({ input }) => {
      // perform creation, e.g., DB call
      return { id: 'generated-id', name: input.name };
    }),
});

export type AppRouter = typeof appRouter;

2. Set up the server

Integrate the router with your backend framework. Example using Express:


// src/server/index.ts
import express from 'express';
import * as trpcExpress from '@trpc/server/adapters/express';
import { appRouter } from './router';

const app = express();

app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({
    router: appRouter,
    createContext: () => ({}),
  }),
);

app.listen(4000, () => {
  console.log('Server listening on http://localhost:4000');
});

3. Consume the API on the client

Import types and create a tRPC client aligned with the server’s router for type-safe calls:


// src/client.ts
import { createTRPCProxyClient, httpLink } from '@trpc/client';
import type { AppRouter } from '../server/router';

const trpc = createTRPCProxyClient({
  links: [httpLink({ url: 'http://localhost:4000/trpc' })],
});

// Call a query
async function fetchUser() {
  const user = await trpc.getUser.query({ id: '123' });
  console.log(user.name); // Type inferred as string
}

// Call a mutation
async function addUser() {
  const newUser = await trpc.createUser.mutation({ name: 'Bob' });
  console.log(newUser.id);
}

Validation

Validation with zod is the canonical way to type inputs and outputs in tRPC:

  • Define schemas using Zod primitives and pipelines in procedure inputs.
  • tRPC enforces these at runtime, automating input sanitisation and error response.
  • You can also parse outputs if desired, though this is less common.

Example input validation (already shown above):


.input(z.object({ id: z.string() }))

Custom validation rules, unions, transformations, and async refinements are fully supported by Zod. This offers flexibility without losing type safety.

Common pitfalls

  • Version mismatches: Always keep server and client tRPC packages at compatible versions. Automatic inference relies on shared types.
  • Incorrect context typing: The createContext function should faithfully type your context if you access sessions, DB, or auth info. Avoid defaulting to any.
  • Too much logic in procedures: Keep procedure resolvers concise and delegate complex business logic to services; this improves testability.
  • Ignoring error handling: Use tRPC’s built-in error shape (TRPCError) to standardise error messages and avoid leaking sensitive data.
  • Client caching and fetching: tRPC only provides calls; to simplify React caching, use the official React Query integration (@trpc/react-query) rather than custom caching.

When to choose tRPC vs alternatives

  • tRPC is ideal for full TypeScript stacks wanting zero-codegen, end-to-end type safety, and simple RPC routing.
  • REST APIs: Opt if your API must be language-agnostic, consumed by non-TypeScript clients, or requires caching/CDN edge cases.
  • GraphQL: Choose if you need complex querying, partial fetching, or tooling ecosystem around schemas and introspection.
  • OpenAPI/Swagger: Useful for standard REST API contracts and broad language support.

In summary, tRPC strikes a strong balance for internal teams or greenfield TS projects with tight client-server coupling.

Checklist / TL;DR

  • ✔️ Install and pin compatible @trpc/server and @trpc/client (v10 or v11 stable).
  • ✔️ Define typed routers and procedures using initTRPC.
  • ✔️ Use Zod for runtime validation of inputs.
  • ✔️ Integrate with your backend HTTP framework using official middleware adapters.
  • ✔️ Share router types with the client for full inference — no codegen needed.
  • ✔️ Handle errors gracefully with TRPCError.
  • ✔️ For React apps, consider @trpc/react-query for powerful data fetching and caching.
  • ⚠️ Keep procedure logic thin; delegate to services or layers.
  • ⚠️ Be mindful of context typing for auth or DB connections.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Post