tRPC 路由
什么是 tRPC?
tRPC 是一个让你在 TypeScript 全栈应用中实现端到端类型安全 API 的框架。与 REST 或 GraphQL 不同,tRPC 不需要代码生成、schema 定义或序列化层 — 你在服务端定义函数,客户端自动获得完整的类型推断。这意味着当你修改 API 签名时,TypeScript 编译器会立即在客户端报错,从根本上消除了 API 层的运行时类型错误。
tRPC 特别适合 Next.js、Remix 等 TypeScript 全栈框架,但也可以和 Express、Fastify 等任何 Node.js 服务端框架配合使用。
快速开始
安装依赖
npm install @trpc/server @trpc/client @trpc/react-query zod
npm install @tanstack/react-query
初始化 tRPC
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
const t = initTRPC.context<Context>().create({
transformer: superjson,
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;
创建第一个路由
// server/routers/greeting.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
export const greetingRouter = router({
hello: publicProcedure
.input(z.object({ name: z.string().optional() }))
.query(({ input }) => {
return { message: `Hello ${input.name ?? 'world'}!` };
}),
});
导出 AppRouter 类型
// server/root.ts
import { router } from './trpc';
import { greetingRouter } from './routers/greeting';
export const appRouter = router({
greeting: greetingRouter,
});
// 仅导出类型 — 不导出运行时代码
export type AppRouter = typeof appRouter;
核心概念
Procedures(过程)
Procedure 是 tRPC 中暴露给客户端的函数。有三种类型:query(读取数据)、mutation(写入数据)、subscription(实时数据推送)。
Query — 读取数据
// 查询用户列表,支持分页
userList: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(20),
cursor: z.string().nullish(),
}))
.query(async ({ input, ctx }) => {
const items = await ctx.db.user.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
let nextCursor: string | undefined;
if (items.length > input.limit) {
nextCursor = items.pop()!.id;
}
return { items, nextCursor };
}),
Mutation — 写入数据
// 创建新用户
createUser: protectedProcedure
.input(z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['USER', 'ADMIN']).default('USER'),
}))
.mutation(async ({ input, ctx }) => {
const user = await ctx.db.user.create({ data: input });
return user;
}),
Subscription — 实时数据
import { observable } from '@trpc/server/observable';
onNewMessage: publicProcedure
.input(z.object({ channelId: z.string() }))
.subscription(({ input }) => {
return observable<Message>((emit) => {
const handler = (msg: Message) => emit.next(msg);
messageEmitter.on(`channel:${input.channelId}`, handler);
return () => {
messageEmitter.off(`channel:${input.channelId}`, handler);
};
});
}),
Zod 输入验证
tRPC 与 Zod 深度集成,所有输入都在运行时验证并自动推断类型。
基本 schema
const createPostInput = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().default(false),
});
嵌套对象
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{5,6}$/),
country: z.string().length(2),
});
const userInput = z.object({
name: z.string(),
address: addressSchema,
billingAddress: addressSchema.optional(),
});
数组验证
const batchInput = z.object({
ids: z.array(z.string().uuid()).min(1).max(50),
tags: z.array(z.string()).max(10).default([]),
});
组合输入(.input().input())
// 多次调用 .input() 会自动合并 schema
updatePost: protectedProcedure
.input(z.object({ id: z.string() }))
.input(z.object({ title: z.string(), content: z.string() }))
.mutation(({ input }) => {
// input 类型: { id: string; title: string; content: string }
}),
Context(上下文)
Context 是每个请求共享的对象,通常包含数据库连接、当前用户会话等。它在中间件和 procedure 中通过 ctx 访问。
创建 Context
// server/context.ts
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getServerSession } from 'next-auth';
import { prisma } from './db';
export async function createContext({ req, res }: CreateNextContextOptions) {
const session = await getServerSession(req, res, authOptions);
return {
db: prisma,
session,
req,
res,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
在 Procedure 中访问 Context
getProfile: protectedProcedure.query(({ ctx }) => {
// ctx.session.user 在 protectedProcedure 中确保存在
return ctx.db.user.findUnique({
where: { id: ctx.session.user.id },
select: { id: true, name: true, email: true, avatar: true },
});
}),
中间件
中间件可以在 procedure 执行前后运行逻辑,用于认证检查、日志记录、性能监控等。
认证中间件(Protected Procedure)
const isAuthed = middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
session: { ...ctx.session, user: ctx.session.user },
},
});
});
export const protectedProcedure = publicProcedure.use(isAuthed);
日志中间件
const logger = middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
if (result.ok) {
console.log(`[${type}] ${path} - ${duration}ms OK`);
} else {
console.error(`[${type}] ${path} - ${duration}ms ERROR`);
}
return result;
});
耗时监控中间件
const timing = middleware(async ({ path, next }) => {
const start = performance.now();
const result = await next();
const elapsed = performance.now() - start;
if (elapsed > 1000) {
console.warn(`Slow procedure: ${path} took ${elapsed.toFixed(0)}ms`);
}
return result;
});
链式组合中间件
// 中间件按 .use() 调用顺序依次执行
export const adminProcedure = publicProcedure
.use(logger)
.use(timing)
.use(isAuthed)
.use(isAdmin); // 每个中间件可以扩展/收窄 ctx 类型
路由组织
嵌套路由
// server/routers/user.ts
export const userRouter = router({
list: publicProcedure.query(/* ... */),
byId: publicProcedure.input(z.string()).query(/* ... */),
create: protectedProcedure.input(createUserSchema).mutation(/* ... */),
update: protectedProcedure.input(updateUserSchema).mutation(/* ... */),
delete: adminProcedure.input(z.string()).mutation(/* ... */),
});
// server/routers/post.ts
export const postRouter = router({
list: publicProcedure.query(/* ... */),
bySlug: publicProcedure.input(z.string()).query(/* ... */),
create: protectedProcedure.input(createPostSchema).mutation(/* ... */),
});
合并路由
// server/root.ts
import { router } from './trpc';
import { userRouter } from './routers/user';
import { postRouter } from './routers/post';
import { commentRouter } from './routers/comment';
export const appRouter = router({
user: userRouter,
post: postRouter,
comment: commentRouter,
});
export type AppRouter = typeof appRouter;
推荐的文件结构
框架集成
| 框架 | 适配器 | 包名 |
|---|---|---|
| Next.js Pages Router | createNextApiHandler | @trpc/server/adapters/next |
| Next.js App Router | fetchRequestHandler | @trpc/server/adapters/fetch |
| Express | createExpressMiddleware | @trpc/server/adapters/express |
| Fastify | fastifyTRPCPlugin | @trpc/server/adapters/fastify |
| Standalone (Node) | createHTTPServer | @trpc/server/adapters/standalone |
Next.js Pages Router
// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '~/server/root';
import { createContext } from '~/server/context';
export default createNextApiHandler({
router: appRouter,
createContext,
onError: ({ error, path }) => {
if (error.code === 'INTERNAL_SERVER_ERROR') {
console.error(`tRPC error on ${path}:`, error);
}
},
});
Next.js App Router
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '~/server/root';
import { createContext } from '~/server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };
Express 适配器
import express from 'express';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { appRouter } from './root';
import { createContext } from './context';
const app = express();
app.use(
'/api/trpc',
createExpressMiddleware({ router: appRouter, createContext })
);
app.listen(3000);
Fastify 适配器
import Fastify from 'fastify';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import { appRouter } from './root';
import { createContext } from './context';
const server = Fastify();
server.register(fastifyTRPCPlugin, {
prefix: '/api/trpc',
trpcOptions: { router: appRouter, createContext },
});
server.listen({ port: 3000 });
错误处理
TRPCError 错误码
| 错误码 | HTTP 状态 | 使用场景 |
|---|---|---|
| BAD_REQUEST | 400 | 输入数据无效 |
| UNAUTHORIZED | 401 | 未登录 |
| FORBIDDEN | 403 | 权限不足 |
| NOT_FOUND | 404 | 资源不存在 |
| CONFLICT | 409 | 资源冲突(如重复创建) |
| TOO_MANY_REQUESTS | 429 | 请求频率超限 |
| INTERNAL_SERVER_ERROR | 500 | 服务器内部错误 |
| TIMEOUT | 408 | 请求超时 |
抛出 TRPCError
import { TRPCError } from '@trpc/server';
getPost: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
const post = await ctx.db.post.findUnique({ where: { id: input.id } });
if (!post) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `Post with id "${input.id}" not found`,
});
}
return post;
}),
自定义错误格式
import { ZodError } from 'zod';
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
客户端错误处理
// React Query 风格
const { data, error } = trpc.post.byId.useQuery({ id: '123' });
if (error) {
if (error.data?.code === 'NOT_FOUND') {
return <NotFound />;
}
// Zod 验证错误
if (error.data?.zodError) {
const fieldErrors = error.data.zodError.fieldErrors;
// 渲染字段级错误...
}
}
高级特性
Output Transformer(superjson)
默认 JSON 不支持 Date、Map、Set 等类型。使用 superjson 可以自动序列化/反序列化这些类型。
import superjson from 'superjson';
// 服务端
const t = initTRPC.context<Context>().create({
transformer: superjson,
});
// 客户端也需要配置相同的 transformer
import { createTRPCClient, httpBatchLink } from '@trpc/client';
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: '/api/trpc', transformer: superjson })],
});
Procedure 元数据
type Meta = { requiresAuth: boolean; rateLimit?: number };
const t = initTRPC
.meta<Meta>()
.context<Context>()
.create();
// 使用元数据
publicProcedure
.meta({ requiresAuth: false, rateLimit: 100 })
.query(() => { /* ... */ });
// 在中间件中读取
const rateLimiter = middleware(async ({ meta, next }) => {
if (meta?.rateLimit) {
// 执行限流逻辑...
}
return next();
});
批量请求
// 客户端默认使用 httpBatchLink
// 同一 tick 内的多个请求会自动合并为一个 HTTP 请求
import { httpBatchLink } from '@trpc/client';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
maxURLLength: 2083, // GET 请求 URL 长度限制
}),
],
});
服务端直接调用(Server-Side Caller)
// 在 Server Components 或后端逻辑中直接调用 procedure
const caller = appRouter.createCaller({
session: null,
db: prisma,
});
// 不走 HTTP,直接执行
const users = await caller.user.list({ limit: 10 });
const post = await caller.post.bySlug('hello-world');
Links(splitLink 条件路由)
import { splitLink, httpBatchLink, httpLink } from '@trpc/client';
const client = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: wsLink({ url: 'ws://localhost:3001' }),
false: httpBatchLink({ url: '/api/trpc' }),
}),
],
});
常见问题 (FAQ)
tRPC 适合 TypeScript 全栈项目,提供零成本的端到端类型安全。REST 适合公开 API 和多语言客户端。GraphQL 适合复杂数据图和需要精确字段选择的场景。如果前后端都用 TypeScript,tRPC 是开发效率最高的选择。
当然可以。tRPC 提供 Express、Fastify、Standalone HTTP、Fetch API 等多种适配器。任何 Node.js 服务端框架都可以使用 tRPC,客户端也可以用 vanilla JS 或 React Query 以外的方案。
tRPC 不原生支持 multipart/form-data。推荐方案:(1) 使用单独的 REST 端点处理上传,返回 URL 后通过 tRPC mutation 保存元数据;(2) 使用 presigned URL 直传到 S3/R2,再通过 tRPC 记录文件信息。
在中间件层实现。可以使用 Procedure 的 meta 字段标记限流配置,在中间件中读取 meta 并调用 Redis 或内存限流器。也可以在 Express/Fastify 的适配器层面统一添加 express-rate-limit 等中间件。
使用 appRouter.createCaller() 创建服务端调用者,传入模拟的 context(如测试数据库连接、假 session),然后像调用普通函数一样测试每个 procedure。不需要启动 HTTP 服务器。