10.1 路由拆分:按领域组织
生产级项目应将 Router 按业务领域拆分,每个领域一个文件。这样能保持文件大小可控,便于团队协作和 Code Review:
server/
├── trpc.ts # initTRPC,procedure 类型
├── context.ts # createContext
├── router.ts # 根 Router,合并所有子 Router
└── routers/
├── auth.ts # 注册、登录、登出
├── user.ts # 用户资料、设置
├── post.ts # 文章 CRUD
├── comment.ts # 评论
├── notification.ts # 通知(含 Subscription)
└── admin/
├── index.ts # 管理员根 Router
├── users.ts # 用户管理
└── content.ts # 内容审核
STRUCTURE
// server/router.ts — 根 Router
import { router } from './trpc';
import { authRouter } from './routers/auth';
import { userRouter } from './routers/user';
import { postRouter } from './routers/post';
import { adminRouter } from './routers/admin';
export const appRouter = router({
auth: authRouter,
user: userRouter,
post: postRouter,
admin: adminRouter,
});
export type AppRouter = typeof appRouter;
TS
10.2 统一错误格式化
生产环境需要统一的错误响应格式,方便前端统一处理,同时屏蔽服务器内部错误细节:
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
// 提取 Zod 验证错误,前端可以精确展示字段错误
zodError:
error.cause instanceof ZodError
? error.cause.flatten()
: null,
// 生产环境屏蔽堆栈信息
stack: process.env.NODE_ENV === 'development'
? shape.data.stack
: undefined,
},
};
},
});
TS
前端统一错误处理
// lib/error-handler.ts
import { TRPCClientError } from '@trpc/client';
import type { AppRouter } from '@/server/router';
export function handleTRPCError(error: unknown) {
if (error instanceof TRPCClientError<AppRouter>) {
const { data } = error;
// 字段级验证错误
if (data?.zodError) {
const fieldErrors = data.zodError.fieldErrors;
return Object.entries(fieldErrors)
.map(([field, errors]) => `${field}: ${errors?.join(', ')}`)
.join('\n');
}
// 业务错误(UNAUTHORIZED、FORBIDDEN 等)
return error.message;
}
return '发生未知错误,请稍后重试';
}
TS
10.3 日志与可观测性:OpenTelemetry
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('trpc-server');
const otelMiddleware = t.middleware(async ({ path, type, next }) => {
const span = tracer.startSpan(`trpc.${type}.${path}`, {
attributes: {
'trpc.path': path,
'trpc.type': type,
},
});
try {
const result = await next();
if (!result.ok) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(result.error);
}
return result;
} finally {
span.end();
}
});
TS
10.4 批量请求优化
httpBatchLink(tRPC 默认)会将同一 tick 内的多个请求合并成一个 HTTP 请求,大幅减少网络往返次数:
// 默认行为:3个 useQuery 合并为 1 次 HTTP 请求
// GET /api/trpc/user.getMe,post.list,notification.count?batch=1&input={...}
// 自定义批量配置
httpBatchLink({
url: '/api/trpc',
maxURLLength: 2083, // URL 超长时自动拆分为多个请求
})
// 调试时禁用批量(更易查看单个请求)
httpLink({ url: '/api/trpc' })
// 按条件切换(开发用 httpLink,生产用 httpBatchLink)
process.env.NODE_ENV === 'development'
? httpLink({ url: '/api/trpc' })
: httpBatchLink({ url: '/api/trpc' })
TS
10.5 文件上传方案
tRPC 基于 JSON 传输,不直接支持文件上传。生产推荐方案是 Presigned URL:前端从 tRPC 获取预签名上传 URL,直接将文件上传到 S3/OSS,避免文件流经过应用服务器。
// server/routers/upload.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'us-east-1' });
export const uploadRouter = router({
// 获取预签名上传 URL
getUploadUrl: protectedProcedure
.input(z.object({
filename: z.string(),
contentType: z.string().startsWith('image/'),
size: z.number().max(5 * 1024 * 1024), // 最大 5MB
}))
.mutation(async ({ input, ctx }) => {
const key = `uploads/${ctx.user.id}/${Date.now()}-${input.filename}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
ContentType: input.contentType,
ContentLength: input.size,
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
return { uploadUrl, key, publicUrl: `${process.env.S3_CDN_URL}/${key}` };
}),
});
TS
10.6 部署方案对比
| 平台 | 优势 | 注意事项 |
|---|---|---|
| Vercel | 零配置,Edge Network,与 Next.js 深度集成 | Serverless,不支持 WebSocket(需 Ably/Pusher) |
| Railway | 支持持久进程,WebSocket 友好,简单部署 | 费用按使用量计算 |
| Fly.io | 全球多区域,支持长连接,价格合理 | 需要 Dockerfile |
| AWS Lambda | 极致弹性伸缩,按调用付费 | 冷启动延迟,WebSocket 需 API Gateway |
| 自建 VPS | 完全控制,支持所有功能 | 需要自行处理运维、SSL、负载均衡 |
10.7 从 REST 渐进式迁移到 tRPC
不需要一次性迁移所有 REST API。tRPC 可以与现有 REST API 共存,逐步迁移:
- 安装 tRPC 并配置 Route Handler(
/api/trpc/[trpc]),不影响现有/api/*路由 - 将新功能直接用 tRPC 开发,旧接口暂时保留为 REST
- 按模块逐步将高频使用的 REST 接口迁移到 tRPC(优先迁移前端代码最多的接口)
- 迁移过程中使用
httpLink的fetch选项将 tRPC 请求代理到旧 REST 接口(过渡方案) - 最终完全切换,删除旧 REST 接口
// 过渡方案:tRPC Procedure 内部调用旧 REST 接口
getLegacyData: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
// 调用旧 REST 接口,返回类型安全的数据
const res = await fetch(`/api/legacy/data/${input.id}`);
const data = await res.json();
// 用 Zod 验证并转换旧接口数据格式
return LegacyDataSchema.parse(data);
}),
TS
生产清单上线前检查:① 错误格式化已配置,生产环境不暴露堆栈信息;② 认证中间件覆盖所有敏感接口;③ 限流中间件保护高风险接口(登录、注册、发送验证码);④ httpBatchLink 开启(减少网络请求);⑤ OpenTelemetry 或至少基础日志中间件已配置;⑥ 健康检查接口(health.ping)已添加。
10.8 tRPC v11 新特性速览
| 特性 | 说明 |
|---|---|
| shorthand router | 更简洁的 router 定义语法 |
| type-safe errors | 完全类型化的错误对象,含 cause 链 |
| 迭代器支持 | AsyncIterable 替代 Observable,原生 TS 支持 |
| Server-Sent Events | 内置 SSE Link,无需 WebSocket 即可实现实时功能 |
| HTTP 流式响应 | 长时间运行的查询支持流式返回部分结果 |
| 更好的 tanstack query 集成 | 与 TanStack Query v5 深度对齐 |
课程总结tRPC 通过 TypeScript 类型系统在前后端之间建立了"零成本类型契约":后端定义 Procedure,类型自动流向前端,无需任何代码生成。核心架构是 initTRPC → Router/Procedure → Context/Middleware → AppRouter 类型导出。客户端通过 createTRPCReact<AppRouter> 获得完整类型推断。生产实践的关键是:按领域拆分 Router、Procedure Builder 封装通用逻辑、统一错误格式化、OpenTelemetry 可观测性。对于全栈 TypeScript 项目,tRPC 是目前开发体验最好的 API 方案。