What tRPC Does
- End-to-End Types: Share TypeScript types between client and server automatically
- No Schema: No protobuf, GraphQL schemas, or OpenAPI to maintain
- Input Validation: Built-in Zod/Yup/Valibot validation
- Subscriptions: WebSocket support for real-time updates
- Middleware: Reusable logic for auth, logging, rate limiting
- Error Handling: Typed error responses
- Batching: Automatic request batching to reduce round-trips
- Framework Support: Next.js, Express, Fastify, AWS Lambda, Cloudflare Workers
- Client Support: React, Vue, vanilla TS, Svelte, SolidJS
- Streaming: Server-sent events for streamed responses
How It Works
Backend (TypeScript) Frontend (TypeScript)
────────────────── ──────────────────
1. Define procedures ──▶ 4. Import types
2. Define inputs 5. Call with autocomplete
3. Return data 6. Full type safety
No schemas. No codegen. Just TypeScript.Example: Next.js App
1. Define Router (server)
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;// server/routers/_app.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
export const appRouter = router({
// Query (GET equivalent)
getUser: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input.id } });
return user;
}),
// Mutation (POST/PUT/DELETE equivalent)
createPost: publicProcedure
.input(z.object({
title: z.string().min(1).max(100),
content: z.string().min(10),
published: z.boolean().default(false),
}))
.mutation(async ({ input }) => {
const post = await db.post.create({ data: input });
return post;
}),
// List with pagination
listPosts: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().nullish(),
}))
.query(async ({ input }) => {
const posts = await db.post.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
let nextCursor: typeof input.cursor = undefined;
if (posts.length > input.limit) {
const nextItem = posts.pop();
nextCursor = nextItem?.id;
}
return { posts, nextCursor };
}),
});
// Export type for client
export type AppRouter = typeof appRouter;2. Client Setup
// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();// pages/_app.tsx
import { httpBatchLink } from '@trpc/client';
import { trpc } from '../lib/trpc';
function MyApp({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</trpc.Provider>
);
}3. Use in Components (Full Type Safety!)
// components/UserProfile.tsx
import { trpc } from '../lib/trpc';
export function UserProfile({ userId }: { userId: string }) {
// Fully typed query - autocomplete for 'getUser'
const { data: user, isLoading } = trpc.getUser.useQuery({ id: userId });
// Fully typed mutation
const createPost = trpc.createPost.useMutation({
onSuccess: () => {
// Auto-refetch user data
utils.getUser.invalidate({ id: userId });
},
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{user?.name}</h1>
<button
onClick={() => createPost.mutate({
title: 'New Post',
content: 'Content here',
published: true,
})}
>
Create Post
</button>
</div>
);
}Magic: If you rename a field in the backend, TypeScript immediately shows errors in every component using that field.
Key Features
Middleware
import { TRPCError } from '@trpc/server';
// Auth middleware
const authMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: { ...ctx, user: ctx.user }, // Type narrowed to non-null
});
});
// Protected procedure
const protectedProcedure = t.procedure.use(authMiddleware);
// Use in router
export const appRouter = router({
getMyPosts: protectedProcedure.query(({ ctx }) => {
return db.post.findMany({ where: { userId: ctx.user.id } });
}),
});Subscriptions (WebSocket)
// Server
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';
const ee = new EventEmitter();
export const appRouter = router({
onNewMessage: publicProcedure.subscription(() => {
return observable<{ text: string }>((emit) => {
const handler = (data: { text: string }) => emit.next(data);
ee.on('message', handler);
return () => ee.off('message', handler);
});
}),
});
// Client
trpc.onNewMessage.useSubscription(undefined, {
onData: (message) => {
console.log('New message:', message.text);
},
});Batching
// Multiple queries in one HTTP request
httpBatchLink({
url: '/api/trpc',
maxURLLength: 2083, // Auto-batch when possible
});// These three queries make 1 HTTP request instead of 3
const user = trpc.user.getById.useQuery({ id: '1' });
const posts = trpc.post.list.useQuery({ userId: '1' });
const stats = trpc.stats.getUser.useQuery({ id: '1' });tRPC vs Alternatives
| Feature | tRPC | GraphQL | REST | gRPC |
|---|---|---|---|---|
| Type safety | End-to-end | Via codegen | Manual | Via codegen |
| Schema | TypeScript | SDL | OpenAPI | Protobuf |
| Setup | Very simple | Moderate | Simple | Complex |
| Client generation | Not needed | Required | Optional | Required |
| Runtime overhead | None | Resolver layer | None | Protobuf parser |
| Subscriptions | Yes (WS) | Yes (WS) | SSE | Streaming |
| Language support | TypeScript only | Many | Any | Many |
| Best for | TS monorepos | Multi-language | Public APIs | Microservices |
Framework Integrations
// Next.js
import { createNextApiHandler } from '@trpc/server/adapters/next';
export default createNextApiHandler({ router: appRouter });
// Express
import { createExpressMiddleware } from '@trpc/server/adapters/express';
app.use('/trpc', createExpressMiddleware({ router: appRouter }));
// Fastify
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
server.register(fastifyTRPCPlugin, { prefix: '/trpc', trpcOptions: { router } });
// Cloudflare Workers
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
export default {
fetch(request: Request) {
return fetchRequestHandler({ endpoint: '/trpc', req: request, router: appRouter });
},
};常见问题
Q: tRPC 和 GraphQL 怎么选? A: 如果你用纯 TypeScript 且前后端在同一个 monorepo,tRPC 更简单更快。如果你需要多语言客户端、复杂查询灵活性或已有 GraphQL 生态,选 GraphQL。tRPC 适合 80% 的内部应用,GraphQL 适合公共 API。
Q: 类型共享怎么实现的? A: tRPC 使用 TypeScript 类型推断。你在 server 定义 router,export AppRouter 类型,client 导入这个类型。编译时 TypeScript 就知道所有 procedure 的输入输出类型——无需运行时开销或代码生成。
Q: 适合移动端吗? A: 可以用于 React Native。tRPC 也支持 vanilla TypeScript 客户端,可以在任何 TypeScript 环境使用。不支持非 TypeScript 客户端(Python/Go/Java)——如果需要,GraphQL 或 REST 更合适。