Skip to content
Innovialabs

tRPC Explained: End-to-End Type-Safe APIs Without REST

Advanced4 min

In short

tRPC lets a TypeScript client call server functions directly, inferring input and output types from the server code with no schema files or code generation; it shines when one team owns both sides in a TypeScript monorepo and is the wrong choice for public or multi-language APIs.

How tRPC works

tRPC (TypeScript Remote Procedure Call) is a library for building APIs where the client calls server functions as if they were local. You define procedures on the server, group them into a router, and export only the router's type. The client imports that type and gets full autocomplete and type checking for every call.

There is no OpenAPI spec, no GraphQL schema and no code generation step. Types flow through TypeScript inference: change a field on the server, and every client call that relies on it shows a compile error immediately. Under the hood requests are still regular HTTP calls with JSON, so tRPC runs on Node.js, edge runtimes and in frameworks like Next.js.

Routers and procedures with validation

A procedure is either a query (reading data) or a mutation (changing data). Input is validated at runtime with a schema library such as Zod, and the same schema provides the TypeScript type.

// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
import { db } from "./db"; // your database client

const t = initTRPC.create();

export const appRouter = t.router({
  getProduct: t.procedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => db.product.findUnique({ where: { id: input.id } })),

  createReview: t.procedure
    .input(z.object({ productId: z.string(), text: z.string().min(10) }))
    .mutation(({ input }) => db.review.create({ data: input })),
});

export type AppRouter = typeof appRouter;

On the client you create a typed client from AppRouter:

import type { AppRouter } from "../server/trpc";
// a client created with AppRouter knows every procedure:
const product = await trpc.getProduct.query({ id: "42" }); // typed result

Key building blocks you will use in practice:

  • Context — per-request data such as the current user or database connection.
  • Middleware — reusable logic, for example a protectedProcedure that rejects unauthenticated calls.
  • Nested routers — user, product, order routers merged into one app router.
  • React Query integration — caching, loading states and invalidation for React apps.

Exact client setup differs between adapters and major versions, so follow the official documentation for your stack.

tRPC vs REST vs GraphQL

tRPCRESTGraphQL
ContractTypeScript types inferred from codeURLs and HTTP methods, optional OpenAPISchema with a type system
Client languagesTypeScript only, in practiceAnyAny
Code generationNot neededOptional (from OpenAPI)Usually used for typed clients
HTTP cachingPossible for queries, less naturalNative and well understoodHarder, usually client-side
Flexible data fetchingFixed per procedureFixed per endpointClient chooses fields
Best fitFull-stack TypeScript appsPublic and integration APIsMany clients with different data needs

tRPC is not a replacement for REST or GraphQL in general; it removes a layer when both ends are written in TypeScript by the same team.

When tRPC is the wrong choice

  • Public APIs for partners or third-party developers. They need stable, documented, language-neutral contracts.
  • Clients in other languages: native iOS (Swift), Android (Kotlin), Python services, 1C.
  • Separate repositories where the client cannot import server types without publishing packages.
  • Long-lived versioned APIs — tRPC encourages changing the contract freely, which breaks old app versions still in use, such as mobile apps not yet updated.
  • Heavy reliance on CDN/HTTP caching of API responses.

A common hybrid: tRPC for your own web app, plus a small REST or webhook layer for external integrations.

Common mistakes

  • Skipping input validation because "TypeScript already checks it". Types disappear at runtime; validation protects the server from any client.
  • Returning raw database models and leaking internal fields.
  • Putting all procedures into one giant router file.
  • Exporting server code instead of only the router type to the client.

FAQ

Not strictly, but it is the most comfortable setup. The client needs access to the router type, which is easiest when server and client live in the same repository or a shared workspace.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.