Learning tRPC - Core Concepts & Main Architecture
Episode 2 of 19

Learning tRPC - Core Concepts & Main Architecture

This episode breaks down tRPC's architecture: how TypeScript infers types from the server to the client, the role of router, procedure, input, output, and caller, as well as the adapter layer for HTTP, WebSocket, and serverless transport along with the proxy client and React client.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Episode 2 dives into the most important part before writing code: tRPC's architecture. You already know why tRPC exists; now it is time to understand how it works behind the scenes — how types flow from the server to the client, what components make up an API, and how tRPC connects to HTTP, WebSocket, or serverless environments.

After this episode, terms like router, procedure, input, output, caller, and links will no longer be foreign. This is the vocabulary used throughout the whole series.

How It Works Behind the Scenes

Type Inference from Server to Client

The key to tRPC is TypeScript inference. When you define a router on the server, its complete type — procedure names, input shapes, and output shapes — is stored in an AppRouter type. The client uses this type as a type argument when creating a proxy client, so the editor knows exactly what is available.

Notice the flow in one simple diagram:

tRPC type flow
server router → type AppRouter → client proxy → autocompletion

There is no extra step like codegen. Types are simply read from the server definition, and TypeScript keeps them in sync every time a procedure changes. This is why tRPC is called zero-codegen and zero-schema-duplication.

Router, Procedure, and Transport Structure

A tRPC API is composed of three layers:

  • Router: a collection of procedures and other combined routers that form the API contract.
  • Procedure: the smallest execution unit — a query, mutation, or subscription.
  • Transport: the adapter that connects a router to a real protocol such as HTTP or WebSocket.

This transport layer is pluggable, so the same router can be served by a standalone server, Express, Fastify, Next.js, or serverless without changing the procedure definitions.

Core Components and How They Work

Router, Procedure, Input, and Output

Let's look at the core components in one real example:

tRPC core components
import { initTRPC } from "@trpc/server";
import { z } from "zod";
 
const t = initTRPC.create();
 
export const appRouter = t.router({
  user: t.router({
    byId: t.procedure
      .input(z.object({ id: z.number() }))
      .query(({ input }) => ({ id: input.id, nama: "Arman" })),
    create: t.procedure
      .input(z.object({ nama: z.string().min(1) }))
      .mutation(({ input }) => ({ id: 2, nama: input.nama })),
  }),
});
 
export type AppRouter = typeof appRouter;

The important parts:

  • t.router({...}) defines a router, and routers can be nested like user.
  • t.procedure is the basic building block; it ends with .query(), .mutation(), or .subscription().
  • .input(schema) validates and types the incoming input.
  • The output comes from the value returned by the resolver, and its type is inferred automatically.

In the code above, appRouter.user.byId.query({ id: 1 }) will return an object typed as { id: number; nama: string }.

Transport Adapters: HTTP, WebSocket, and Serverless

A router knows nothing about protocols. An adapter connects it to the outside world:

  • HTTP: createHTTPServer for a standalone server, createExpressMiddleware for Express, fetchRequestHandler for Next.js and serverless.
  • WebSocket: wsServer (called applyWSSHandler in v10) for real-time subscriptions.
  • Serverless: fetch adapters that run on Vercel, Cloudflare, or AWS Lambda.

The consequence is big: the procedure code stays the same, only the adapter changes depending on the deployment. Episodes 10 and 16 will discuss both in detail.

Client-Side Generation

On the client side, tRPC provides two ways to access the same router:

Two kinds of tRPC clients
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
 
const trpc = createTRPCProxyClient<AppRouter>({
  links: [httpBatchLink({ url: "http://localhost:3000/trpc" })],
});
 
const api = createTRPCReact<AppRouter>();
  • createTRPCProxyClient is used outside React — terminals, scripts, or vanilla JS.
  • createTRPCReact generates hooks like api.user.byId.useQuery() inside React.

Both accept AppRouter as a type argument, so autocompletion and type-safety fully apply on the client side.

The Role of Caller in the Architecture

Sometimes you need to call a procedure from within the server itself — for example from a cron job, a webhook handler, or when preparing data on the server. This is where caller plays its role:

Calling a procedure from the server
import { createCallerFactory } from "@trpc/server";
 
const createCaller = createCallerFactory(appRouter);
const caller = createCaller({});
 
const user = await caller.user.byId({ id: 5 });
console.log(user.nama);

createCallerFactory(appRouter) produces a caller function that executes procedures without an HTTP round-trip, but still runs middleware and validation. This pattern is important for internal services and will be used again in episode 17 for testing.

Tip

Understand the difference: a resolver is a definition, a caller is an execution. The caller executes a procedure as if it came from a client, complete with middleware and input validation.

Conclusion

Episode 2 gives you a map of tRPC's architecture: types flow from the server router to the client through inference, procedures are composed of input, resolver, and output, transport is provided by pluggable adapters, and the caller enables invocation from within the server.

Key takeaways:

  • A router is the API contract; a procedure is the smallest execution unit.
  • .input() validates and types the input; the output is inferred from the resolver.
  • HTTP, WebSocket, and serverless adapters do not change router definitions.
  • createTRPCProxyClient for non-React; createTRPCReact for hooks.
  • caller executes procedures inside the server without a round-trip.
  • AppRouter is exported as a type so the client can infer it.

In the next episode, episode 3, we will go straight to building your first tRPC API — arranging the project structure, defining a router with query, mutation, and subscription, and applying zod validation on both input and output.