tRPC
ActivetRPC (TypeScript Remote Procedure Call) is a TypeScript-first framework for building end-to-end typesafe APIs without code generation or schema files. The API router is defined in TypeScript on the server; the client imports the router type and gets full TypeScript inference for all procedures, inputs, and outputs. tRPC v11 is the current version, heavily used in the Next.js, Remix, and React ecosystem via the T3 Stack.
In one line
tRPC provides end-to-end TypeScript type safety from server to client with zero code generation. You define procedures (queries and mutations) on the server using TypeScript. The client imports only the type (not the runtime code) and TypeScript infers all inputs, outputs, and errors automatically. tRPC v11 uses HTTP for queries/mutations and WebSocket for subscriptions. Validation via Zod, Yup, or Superstruct. Dominant in Next.js fullstack apps (T3 Stack).
Quick Reference
| Field | Size | Description |
|---|---|---|
| Procedure types | query / mutation / subscription | query: read operation (GET semantics). mutation: write operation (POST semantics). subscription: real-time event stream (WebSocket). No verbs in procedure names – use nouns and type implicitly communicates read vs write. |
| Router | createTRPCRouter() | The server-side API definition. Composes procedures into a nested structure. Routers can include sub-routers for modular organization. The AppRouter type is exported and imported by clients. |
| Input validation | Zod, Yup, Superstruct | Every procedure can define a Zod schema for its input. tRPC validates input at runtime and TypeScript infers the validated type. Invalid input returns a 400 PARSE_ERROR. |
| Context | createTRPCContext() | Per-request context: database connections, authenticated user, request headers. Context is created once per request and passed to all procedures. Used for auth, tenancy, logging. |
| Middleware | procedure.use(middleware) | Composable middleware for authentication, authorization, rate limiting, logging. .use() runs before the procedure. t.procedure.use(isAuthed) creates a protected procedure. |
| HTTP transport | GET and POST | Queries use HTTP GET (cacheable). Mutations use HTTP POST. Endpoint: /api/trpc/[procedure]. Batching: multiple queries in one HTTP request (automatic client-side batching). |
| Type safety | Zero codegen | Client imports RouterOutput and RouterInput types from the server router. TypeScript inference works without JSON Schema, Protobuf, or OpenAPI specs. The router definition IS the schema. |
| Error handling | TRPCError | throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' }). Standard codes: PARSE_ERROR, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_SERVER_ERROR. Client catches errors with TRPCClientError. |
Key Characteristics
Zero code generation
Unlike gRPC (protoc) or GraphQL (codegen CLI), tRPC requires no schema file or code generation step. The TypeScript server definition is the contract. Import the AppRouter type on the client and TypeScript does the rest.
Full TypeScript inference
Rename a field in a procedure output and every client call site shows a type error immediately. Add a required input field and every call site that doesn't provide it turns red. The type system catches API contract violations at development time.
TypeScript only
tRPC requires TypeScript on both client and server. Non-TypeScript clients (mobile native, third-party integrations) cannot benefit from type safety. For multi-consumer APIs serving external clients, REST or GraphQL with an OpenAPI/schema provides better interoperability.
T3 Stack standard
tRPC is the API layer in the T3 Stack (create-t3-app: Next.js + TypeScript + Tailwind + Prisma + tRPC + NextAuth). The most popular Next.js fullstack starter by install count uses tRPC as its API mechanism.
Message Format
// Server: define procedures
import { z } from "zod";
import { createTRPCRouter, publicProcedure, protectedProcedure } from "@/server/trpc";
export const userRouter = createTRPCRouter({
// Query: GET /api/trpc/user.byId
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.db.user.findUnique({ where: { id: input.id } });
}),
// Mutation: POST /api/trpc/user.create
create: protectedProcedure
.input(z.object({
name: z.string().min(1),
email: z.string().email(),
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.user.create({ data: input });
}),
// Subscription: WebSocket /api/trpc/user.onUpdate
onUpdate: publicProcedure
.input(z.object({ userId: z.string() }))
.subscription(({ input }) => {
return observable<User>((emit) => {
const unsub = userEventEmitter.on(input.userId, (user) => emit.next(user));
return () => unsub();
});
}),
});// Client: full TypeScript inference, no codegen
import { trpc } from "@/utils/trpc";
// Query – input and output fully typed
const { data: user } = trpc.user.byId.useQuery({ id: "usr_123" });
// ^^ TypeScript knows: { id: string }
// ^^^ TypeScript knows: User | null
// Mutation – typed inputs and outputs
const createUser = trpc.user.create.useMutation();
await createUser.mutateAsync({
name: "Alice", // TypeScript enforces: string, min 1
email: "[email protected]" // TypeScript enforces: valid email
});
// Type error if you pass wrong types or miss required fields
// Subscription
trpc.user.onUpdate.useSubscription(
{ userId: "usr_123" },
{ onData: (user) => console.log(user) }
);
// HTTP wire format (simplified)
// GET /api/trpc/user.byId?input={"json":{"id":"usr_123"}}
// POST /api/trpc/user.create
// Body: {"json":{"name":"Alice","email":"[email protected]"}}
// Response: {"result":{"data":{"json":{"id":"usr_123","name":"Alice"}}}}