你是否经历过这样的场景:前端调用后端接口时,请求参数拼写错误直到运行时才发现;后端改了字段名,前端却毫不知情直到用户反馈页面白屏?据 State of JS 2025 调查,超过 62% 的 TypeScript 开发者表示前后端类型不一致是日常开发中最大的痛点之一。tRPC 正是为解决这个问题而生——它让你在 TypeScript 项目中获得端到端类型安全,无需代码生成、无需 Schema 定义、无需额外的学习成本。
🔐 一、tRPC 核心原理与架构
1.1 为什么 REST 和 GraphQL 不够好
在深入 tRPC 之前,我们需要理解它要解决的核心问题。传统的 API 开发方式存在一个根本性的「类型断裂」——前端和后端是两个独立的类型系统,中间通过 HTTP 请求连接,类型信息在这个过程中完全丢失。
| 对比维度 | REST + OpenAPI | GraphQL + Codegen | tRPC |
|---|---|---|---|
| 类型安全来源 | 手动维护 Swagger | Schema-first | 直接推导 TypeScript 类型 |
| 代码生成 | 需要(openapi-generator) | 需要(graphql-codegen) | ❌ 不需要 |
| 运行时开销 | 低 | 中(查询解析) | 极低(仅 RPC 调用) |
| 学习成本 | 低 | 高(SDL + Resolver) | 低(纯 TypeScript) |
| 前端改后端反馈 | 需要重新生成 | 需要重新生成 | ⚡ 即时类型报错 |
| 适合场景 | 公开 API / 多语言客户端 | 复杂查询 / 多数据源 | TypeScript 全栈 |
⚡ 关键结论: 如果你的前后端都使用 TypeScript,tRPC 是目前类型安全成本最低、开发体验最好的方案。它不是 REST 或 GraphQL 的替代品,而是在 TypeScript 全栈场景下的最优解。
1.2 tRPC 的工作原理
tRPC 的核心思想非常优雅:后端定义的路由和处理器本身就是类型,前端直接消费这些类型,TypeScript 编译器在编译时完成类型检查。
后端 Router 定义
↓ (TypeScript 类型推导)
tRPC Client 类型生成
↓ (IDE 自动补全)
前端类型安全调用
整个过程零运行时、零代码生成。当你修改后端的返回类型时,前端代码中所有不匹配的调用会立即出现红色波浪线。
1.3 快速搭建 tRPC 项目
下面是完整的最小化 tRPC 项目示例,涵盖服务端和客户端:
// server/trpc.ts — 初始化 tRPC 后端
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
// 创建 tRPC 实例,可传入上下文类型
const t = initTRPC.context<Context>().create();
// 导出可复用的 router 和 procedure 构建器
export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;
// server/routers/user.ts — 定义用户路由
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
// 模拟数据库
const users = [
{ id: '1', name: '张三', email: 'zhangsan@example.com', role: 'admin' },
{ id: '2', name: '李四', email: 'lisi@example.com', role: 'user' },
];
export const userRouter = router({
// 查询单个用户
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = users.find(u => u.id === input.id);
if (!user) {
throw new Error('用户不存在');
}
return user; // 返回类型自动推导为 { id: string; name: string; email: string; role: string }
}),
// 创建用户
create: publicProcedure
.input(z.object({
name: z.string().min(2, '名称至少 2 个字符'),
email: z.string().email('邮箱格式不正确'),
role: z.enum(['admin', 'user']).default('user'),
}))
.mutation(async ({ input }) => {
const newUser = { id: String(users.length + 1), ...input };
users.push(newUser);
return newUser;
}),
// 列表查询,支持分页和过滤
list: publicProcedure
.input(z.object({
page: z.number().min(1).default(1),
pageSize: z.number().min(1).max(100).default(10),
role: z.enum(['admin', 'user']).optional(),
}).optional())
.query(({ input }) => {
const { page = 1, pageSize = 10, role } = input ?? {};
let filtered = users;
if (role) {
filtered = filtered.filter(u => u.role === role);
}
const start = (page - 1) * pageSize;
return {
items: filtered.slice(start, start + pageSize),
total: filtered.length,
page,
pageSize,
};
}),
});
// server/index.ts — 组合路由并启动服务
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { router } from './trpc';
import { userRouter } from './routers/user';
// 组合所有子路由
const appRouter = router({
user: userRouter,
});
// 导出 AppRouter 类型供客户端使用
export type AppRouter = typeof appRouter;
// 启动 HTTP 服务
const server = createHTTPServer({
router: appRouter,
});
server.listen(3001);
console.log('tRPC server listening on port 3001');
// client/index.ts — 前端调用(零代码生成,类型直接推导)
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server';
// 创建客户端,泛型直接传入后端 Router 类型
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3001',
}),
],
});
async function main() {
// ✅ 自动补全:输入 input 后 IDE 会提示 { id: string }
const user = await trpc.user.getById.query({ id: '1' });
console.log(user.name); // ✅ 类型安全:user 的类型自动推导
// ❌ 编译时报错:属性 "username" 不存在
// const bad = await trpc.user.getById.query({ username: '1' });
// ✅ 创建用户
const newUser = await trpc.user.create.mutate({
name: '王五',
email: 'wangwu@example.com',
role: 'user',
});
console.log(newUser.id);
}
main();
💡 提示: tRPC 的核心价值在于
export type AppRouter = typeof appRouter这一行代码。它将整个后端路由的类型结构导出,客户端只需要这一个类型就能获得所有路由的完整类型信息。
🚀 二、生产级实战模式
2.1 中间件与权限控制
在实际项目中,API 通常需要认证和授权。tRPC 的中间件(Middleware)系统非常灵活,可以实现精确的权限控制:
// server/middleware.ts — 认证与授权中间件
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
// 定义上下文类型
interface Context {
user?: {
id: string;
role: 'admin' | 'user';
permissions: string[];
};
token?: string;
}
const t = initTRPC.context<Context>().create();
// 认证中间件:要求用户已登录
const isAuthenticated = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: '请先登录',
});
}
return next({
ctx: {
// 将 user 类型从可选变为必需
user: ctx.user,
},
});
});
// 角色检查中间件
const requireRole = (role: 'admin' | 'user') =>
t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
if (ctx.user.role !== role && ctx.user.role !== 'admin') {
throw new TRPCError({
code: 'FORBIDDEN',
message: `需要 ${role} 权限`,
});
}
return next({ ctx: { user: ctx.user } });
});
// 权限检查中间件
const requirePermission = (permission: string) =>
t.middleware(({ ctx, next }) => {
if (!ctx.user?.permissions.includes(permission)) {
throw new TRPCError({
code: 'FORBIDDEN',
message: `缺少权限: ${permission}`,
});
}
return next({ ctx: { user: ctx.user } });
});
// 导出不同权限级别的 procedure
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthenticated);
export const adminProcedure = t.procedure.use(requireRole('admin'));
// 组合使用示例
export const userManageProcedure = t.procedure
.use(isAuthenticated)
.use(requirePermission('user:manage'));
// server/routers/admin.ts — 使用权限中间件
import { z } from 'zod';
import { router } from '../trpc';
import { adminProcedure, protectedProcedure } from '../middleware';
export const adminRouter = router({
// 只有管理员可以查看所有用户
allUsers: adminProcedure.query(async () => {
return await db.user.findMany();
}),
// 登录用户可以更新自己的资料
updateProfile: protectedProcedure
.input(z.object({
name: z.string().min(1).max(50),
bio: z.string().max(500).optional(),
}))
.mutation(async ({ ctx, input }) => {
// ctx.user 保证存在(经过认证中间件)
return await db.user.update({
where: { id: ctx.user.id },
data: input,
});
}),
});
⚠️ 警告: 不要在 tRPC 路由中直接信任客户端输入。即使使用了 Zod 验证,也要在数据库操作层进行二次校验。Zod 只验证结构,不验证业务逻辑(如"该邮箱是否已被注册")。
2.2 错误处理与响应格式
tRPC 内置了结构化的错误处理机制,配合 Zod 的验证错误,可以实现非常友好的错误反馈:
// server/error-handler.ts — 全局错误格式化
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
// Zod 验证错误:提取字段级别的错误信息
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten().fieldErrors
: null,
// 自定义错误码
timestamp: new Date().toISOString(),
},
};
},
});
// 定义业务错误
export class BusinessError extends TRPCError {
constructor(code: string, message: string, public details?: Record<string, unknown>) {
super({ code: 'BAD_REQUEST', message, cause: { code, details } });
}
}
// 使用示例
export const orderRouter = t.router({
create: t.procedure
.input(
z.object({
productId: z.string(),
quantity: z.number().int().positive(),
})
)
.mutation(async ({ input }) => {
const product = await db.product.findUnique({
where: { id: input.productId },
});
if (!product) {
throw new BusinessError('PRODUCT_NOT_FOUND', '商品不存在');
}
if (product.stock < input.quantity) {
throw new BusinessError(
'INSUFFICIENT_STOCK',
`库存不足,当前库存: ${product.stock}`,
{ available: product.stock, requested: input.quantity }
);
}
return await db.order.create({
data: { ...input, userId: ctx.user.id },
});
}),
});
客户端统一处理错误:
// client/error-handler.ts — 前端统一错误处理
import { TRPCClientError } from '@trpc/client';
import type { AppRouter } from '../server';
function handleTRPCError(error: unknown) {
if (error instanceof TRPCClientError) {
const { message, data, shape } = error;
// 处理 Zod 验证错误(字段级别)
if (shape?.data?.zodError) {
const fieldErrors = shape.data.zodError as Record<string, string[]>;
return {
type: 'validation' as const,
fields: fieldErrors,
message: Object.values(fieldErrors).flat().join('; '),
};
}
// 处理认证错误
if (shape?.code === 'UNAUTHORIZED') {
// 跳转到登录页
window.location.href = '/login';
return { type: 'auth' as const, message: '请重新登录' };
}
// 处理业务错误
return { type: 'business' as const, message };
}
// 未知错误
return { type: 'unknown' as const, message: '服务器内部错误' };
}
2.3 订阅与实时数据
tRPC 支持 WebSocket 订阅(Subscription),适合实时数据场景:
// server/routers/notification.ts — 实时通知订阅
import { z } from 'zod';
import { observable } from '@trpc/server/observable';
import { router, protectedProcedure } from '../trpc';
import { EventEmitter } from 'events';
// 全局事件总线(生产环境建议用 Redis Pub/Sub)
const ee = new EventEmitter();
export const notificationRouter = router({
// 订阅当前用户的通知
onNew: protectedProcedure.subscription(({ ctx }) => {
return observable<{ id: string; message: string; createdAt: Date }>((emit) => {
const handler = (data: { userId: string; notification: any }) => {
// 只推送给目标用户
if (data.userId === ctx.user.id) {
emit.next(data.notification);
}
};
ee.on('notification', handler);
// 清理函数:客户端断开时执行
return () => {
ee.off('notification', handler);
};
});
}),
// 标记通知已读
markRead: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
await db.notification.update({
where: { id: input.id, userId: ctx.user.id },
data: { read: true },
});
return { success: true };
}),
});
// 服务端发送通知(其他地方调用)
export function sendNotification(userId: string, message: string) {
ee.emit('notification', {
userId,
notification: {
id: crypto.randomUUID(),
message,
createdAt: new Date(),
},
});
}
💡 三、性能优化与最佳实践
3.1 请求批处理
tRPC 内置了 HTTP Batch Link,可以将多个并发请求合并为一次 HTTP 调用,显著减少网络往返:
// client/trpc.ts — 配置批处理
import { createTRPCClient, httpBatchLink, splitLink, httpLink } from '@trpc/client';
import type { AppRouter } from '../server';
export const trpc = createTRPCClient<AppRouter>({
links: [
// 批处理链路:默认合并请求
splitLink({
condition: (op) => op.type === 'subscription',
// 订阅走 WebSocket
true: httpLink({
url: 'ws://localhost:3001',
}),
// 查询和变更走批处理 HTTP
false: httpBatchLink({
url: 'http://localhost:3001/trpc',
// 自定义请求头
headers: () => ({
Authorization: `Bearer ${getToken()}`,
}),
// 最大批处理数量
maxURLLength: 2048,
}),
}),
],
});
// 并发调用时自动合并为一个请求
async function loadDashboard() {
// 这三个请求会被合并为一次 HTTP 调用
const [user, stats, notifications] = await Promise.all([
trpc.user.profile.query(),
trpc.dashboard.stats.query(),
trpc.notification.recent.query(),
]);
}
3.2 与 Next.js / Nuxt 集成
tRPC 与主流框架的集成非常成熟。以 Next.js App Router 为例:
// server/api/trpc.ts — Next.js 适配
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from './root';
import { createTRPCContext } from './context';
// Next.js Route Handler
export const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: createTRPCContext,
// 生产环境关闭详细错误
onError:
process.env.NODE_ENV === 'development'
? ({ path, error }) => {
console.error(`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`);
}
: undefined,
});
export { handler as GET, handler as POST };
3.3 生产环境注意事项
以下是我在多个生产项目中总结的 tRPC 最佳实践:
✅ 推荐做法:
- 使用
superjson作为数据转换器,自动处理Date、Map、Set等类型 - 为每个子路由单独文件,保持路由树清晰
- 使用
protectedProcedure和adminProcedure分层权限 - 开启
httpBatchLink减少网络请求 - 在 CI 中运行 TypeScript 类型检查,捕获前后端类型不一致
❌ 避免做法:
- 不要在路由处理器中直接写 SQL,保持业务逻辑分层
- 不要忽略
TRPCError的code字段,它会影响 HTTP 状态码 - 不要在客户端手动
JSON.parsetRPC 响应 - 不要在一个路由文件中定义超过 15 个 procedure
📌 记住: tRPC 的类型推导依赖 TypeScript 项目引用(Project References)。如果你的前后端是独立的 monorepo 包,确保正确配置了
tsconfig.json的references和paths,否则客户端无法获取后端类型。
3.4 tRPC 与 AI 集成
在 AI 应用开发中,tRPC 可以优雅地封装 LLM 调用:
// server/routers/ai.ts — AI 辅助路由
import { z } from 'zod';
import { router, protectedProcedure } from '../trpc';
export const aiRouter = router({
// 代码审查
reviewCode: protectedProcedure
.input(z.object({
code: z.string().min(1).max(10000),
language: z.enum(['typescript', 'python', 'java', 'go']),
}))
.mutation(async ({ input }) => {
const result = await llm.chat({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '你是一个代码审查专家,请用中文回复。',
},
{
role: 'user',
content: `请审查以下 ${input.language} 代码:\n\`\`\`${input.language}\n${input.code}\n\`\`\``,
},
],
});
return {
review: result.choices[0].message.content,
model: 'gpt-4o',
tokens: result.usage,
};
}),
});
📊 总结与方案选择建议
经过以上分析,我们可以给出明确的技术选型建议:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| TypeScript 全栈项目 | ✅ tRPC | 零成本类型安全,开发体验最佳 |
| 需要公开 API(多语言客户端) | ✅ REST + OpenAPI | 兼容性好,生态成熟 |
| 复杂嵌套查询 / 多数据源 | ✅ GraphQL | 灵活的查询能力 |
| 微服务间通信 | ✅ tRPC / gRPC | tRPC 适合 TS 微服务,gRPC 适合多语言 |
| 移动端 + Web 共用 API | ✅ REST / GraphQL | 多语言客户端支持 |
tRPC 不是要取代所有 API 方案,而是在 TypeScript 全栈场景下提供了目前最优的开发体验。如果你的团队前后端都使用 TypeScript,且不需要对外暴露 API,tRPC 几乎没有理由不用。
⚡ 关键结论: tRPC 的核心价值不是"少写几行代码",而是从根本上消除了前后端之间的类型鸿沟。当你修改后端接口时,前端代码中的类型错误会在 IDE 中实时高亮——这种开发体验一旦用过就回不去了。
相关工具推荐:
- 🔧 jsjson.com JSON 格式化工具 — 验证 tRPC 响应数据结构
- 🔧 jsjson.com TypeScript 类型检查 — 理解类型推导机制
- 🔧 jsjson.com 正则表达式测试 — 调试 Zod 验证规则中的正则
- 🔧 jsjson.com 时间戳转换 — 处理 tRPC 中
Date类型的序列化问题