在 TypeScript 全栈项目里,前后端类型常常是两个”孤岛”:后端改了字段名,前端编译期毫无察觉,直到运行时白屏。tRPC 用类型安全 API的思路彻底解决这个问题——它不生成代码、不写 OpenAPI,而是让前端直接调用后端 router,参数与返回值的类型从服务端自动推断。本文用一个能跑通的示例,讲清 tRPC 的核心用法、与 Zod 的协作,以及在 Next.js 中的落地方式。
一、为什么需要 tRPC:前后端类型的鸿沟
传统 REST 方案里,前端要”猜”后端返回什么。要么手写一份 interface 手动同步,要么靠 OpenAPI 生成客户端——前者容易漂移,后者引入一整套工具链。更隐蔽的是,TypeScript 的运行时类型校验只在编译期生效,接口响应的真实结构在运行时仍是不可信的。
tRPC 的聪明之处在于:既然前后端都是 TypeScript,那就让类型成为契约本身。你只写一份 router 定义,前端拿到的调用对象就自带完整类型,服务端一改字段,前端调用处立刻红线报错。没有代码生成、没有中间描述文件,类型从服务端”流过”网络直接落到客户端。
二、5 分钟跑通一个 tRPC 服务端
核心只有三步:用 initTRPC 创建上下文、用 z 做入参校验、把多个子 router 合并成 appRouter。下面是一段最小可运行的服务端代码:
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/routers/user.ts
const userRouter = router({
list: publicProcedure.query(async () => {
return [
{ id: 1, name: "Ada" },
{ id: 2, name: "Linus" },
];
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return { id: input.id, name: `user-${input.id}` };
}),
});
export const appRouter = router({
user: userRouter,
});
// 关键:把类型导出给前端,零运行时成本
export type AppRouter = typeof appRouter;
注意最后一行 export type AppRouter——它没有任何运行时开销,只是把 router 的”形状”送给前端。这正是 tRPC 类型安全的来源。
三、前端直接调用:类型自动推断
前端用 @trpc/client 创建一个代理客户端,所有方法、参数、返回值都从 AppRouter 推断而来。服务端改了 getById 的入参,前端如果传错类型,IDE 会立刻标红:
// client.ts
import { createTRPCProxyClient } from "@trpc/client";
import type { AppRouter } from "../server/trpc";
const trpc = createTRPCProxyClient<AppRouter>({
url: "http://localhost:3000/trpc",
});
// 返回值类型自动推断为 { id: number; name: string }[]
const users = await trpc.user.list.query();
// 入参类型强制为 { id: number },传字符串直接编译报错
const one = await trpc.user.getById.query({ id: 2 });
如果你用 React,官方推荐配合 @trpc/react-query 使用,调用变成 trpc.user.list.useQuery(),自动获得缓存、重试与 loading 状态,体验与 React Query 完全一致,但多了一层端到端类型保护。
四、与 Zod 联用:入参校验不缺位
tRPC 的每个 procedure 都用 z(Zod)声明 input,请求进来时自动校验。这把我们在 Zod 运行时类型校验 一文里强调的”边界守卫”理念落到了框架层:非法入参根本进不了业务逻辑。下面对比一下手写校验与 Zod 的差异:
| 维度 | 手写 if 校验 | Zod + tRPC |
| 类型同步 | 需手写 interface,易漂移 | schema 即类型,自动推断 |
| 错误提示 | 自己拼字符串 | error.format() 结构化 |
| 维护成本 | 改两处 | 改一处 |
| 边界覆盖 | 易遗漏字段 | 声明即强制 |
// 复杂入参也能一句话声明
const createPost = publicProcedure
.input(z.object({
title: z.string().min(1).max(100),
tags: z.array(z.string()).max(5),
draft: z.boolean().default(false),
}))
.mutation(async ({ input }) => {
// input 已被校验且类型安全
return { ok: true, id: Date.now() };
});
五、在 Next.js 中落地:全栈一体的 BFF
在 Next.js 与 React Server Components 架构里,tRPC 通常作为 BFF(Backend For Frontend)层。用 App Router 的 Route Handler 暴露一个统一的 /trpc 入口即可:
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/trpc";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };
Server Component 里可以直接 await trpc.user.list.query() 取数,类型同样端到端安全;需要客户端交互的部分再交给 React Query 适配器。这样一套类型贯穿数据库、服务端、前端,是 TypeScript 全栈最顺滑的写法之一。
六、工程化:容器化与类型校验
把 tRPC 服务用 Docker 编排 打包,并在 GitHub Actions 里做类型检查,能防止”本地能跑、CI 崩了”的类型漂移。一个最小多阶段 Dockerfile:
# Dockerfile FROM node:20-alpine AS base WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine WORKDIR /app COPY --from=base /app/node_modules ./node_modules COPY --from=base /app/dist ./dist EXPOSE 3000 CMD ["node", "dist/server.js"]
# .github/workflows/ci.yml
name: ci
on: [push]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run typecheck # tsc --noEmit,守住端到端类型
七、常见坑与排查
| 现象 | 根因 | 解法 |
| 前端类型不更新 | 没导出 / 没重新推导 AppRouter | 确认 export type AppRouter,前端重跑 tsc |
| 入参 400 报错 | Zod 校验失败 | 看 error.format(),检查字段名与类型 |
| 浏览器跨域 | 缺 CORS 头 | 在 adapter 外层加 CORS 中间件 |
| nuqs / 序列化异常 | 入参含 Date 等不可序列化值 | 用 z.coerce 或 transform 归一 |
八、tRPC 适合谁?与 REST / gRPC 怎么选
tRPC 不是银弹。下面这张表帮你在技术选型时快速判断。无论你是用 Vue3 还是 React,只要前后端都是 TypeScript,tRPC 的体验都一致;配合 高效的开发工具链,编辑器里的类型补全会让你彻底告别”猜接口”。
| 维度 | tRPC | REST | gRPC |
| 类型安全 | 端到端自动 | 需手写/生成 | 强(protobuf) |
| 多语言 | 限 TS 两端 | 任意 | 任意 |
| 调试友好度 | 中(JSON over HTTP) | 高(curl 即可) | 低(二进制) |
| 适用场景 | TS 全栈内部 API | 对外开放 API | 内部高性能 RPC |
经验法则:对外暴露给第三方、需要多语言调用的接口,用 REST 或 gRPC;团队内部的 TS 全栈服务,尤其是 BFF 与中后台,tRPC 能把类型红利吃到极致。两者并不互斥,很多项目用 tRPC 做内部、REST 做对外网关。
九、总结
tRPC 的价值不在”又一种 RPC”,而在于它把类型安全 API做到了极致:一份 router 定义,编译期给前端类型、运行期给入参校验,省掉了 OpenAPI 与代码生成两套负担。配合 Zod 在边界做运行时守卫,再放进 Docker 与 CI 的类型检查流水线,你就能用最小的维护成本,守住全栈 TypeScript 项目的类型契约。对于追求开发效率与健壮性的 TS 团队,tRPC 值得作为内部 API 的默认选项。




