告别 API 文档:使用 tRPC 构建端到端类型安全的应用
1. 前后端协作的“契约”难题
在传统的前后端分离开发中,API 就是两者之间的“契约”。我们通常通过以下方式来维护这份契约:
- RESTful API: 依赖 OpenAPI (Swagger) 等工具生成和维护详尽的 API 文档。
- GraphQL: 依赖一个严格的 Schema Definition Language (SDL) 来定义数据结构和操作。
这些方案行之有效,但都存在一个共同的问题:契约与实现是分离的。当前端开发者调用一个 API 时,他相信这个 API 会返回文档或 Schema 中所描述的数据结构。但如果后端实现发生了变更(比如修改了一个字段名)而文档或 Schema 未能及时更新,那么这种不匹配只会在运行时才会暴露出来,导致 Bug。
有没有一种方法,能让这份“契约”自动与实现保持同步,甚至在编译时就能发现不匹配?
2. tRPC 的核心思想:共享类型,而非模式
tRPC (TypeScript Remote Procedure Call) 提出了一个激进但极其简单的方案:如果你的前端和后端都使用 TypeScript,为什么不直接共享类型呢?
tRPC 让你能够编写纯 TypeScript 函数作为后端 API,然后直接在前端调用它们,并享受到完整的类型推断和自动补全,就好像在调用一个本地模块的函数一样。
它不依赖任何 Schema 或代码生成。唯一的“契约”就是你的 TypeScript 类型本身。
3. 它是如何工作的?
tRPC 的魔法在于类型推断和一点巧妙的封装。
a. 后端:定义 API Router
在后端(通常是一个 Node.js 服务),你使用 tRPC 的函数来创建一个或多个“Router”,每个 Router 都是一组可供调用的“Procedure”(即你的 API 端点)。
// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // 使用 Zod 进行运行时校验
const t = initTRPC.create();
export const appRouter = t.router({
// 定义一个名为 `getUser` 的查询 procedure
getUser: t.procedure
.input(z.object({ userId: z.string() }))
.query(({ input }) => {
// 在这里查询数据库或执行其他逻辑
const user = { id: input.userId, name: 'Alex' };
return user;
}),
// 定义一个名为 `createUser` 的变更 procedure
createUser: t.procedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => {
const user = { id: `${Math.random()}`, name: input.name };
return user;
}),
});
// 导出 router 的类型定义
export type AppRouter = typeof appRouter;b. 前端:创建客户端并调用 API
在前端,你只需要从后端导入 AppRouter 这个 类型。注意,你导入的只是类型,而不是任何服务端的代码。
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router'; // 只导入类型
export const trpc = createTRPCReact<AppRouter>();现在,你可以在你的 React 组件中像调用本地函数一样调用 API,并获得完整的类型安全和自动补全!
// client/components/UserInfo.tsx
import { trpc } from '../trpc';
function UserInfo({ userId }: { userId: string }) {
// `useQuery` 的第一个参数是 procedure 的路径
// 你输入 `trpc.` 时,IDE 会自动提示 `getUser` 和 `createUser`
const userQuery = trpc.getUser.useQuery({ userId });
if (userQuery.isLoading) {
return <div>Loading...</div>;
}
// `userQuery.data` 的类型被自动推断为 { id: string; name: string }
return <div>User: {userQuery.data?.name}</div>;
}如果此时,后端的开发者将 getUser 返回的 name 字段重命名为 fullName,前端的 userQuery.data?.name 会立刻在 TypeScript 编译时报错,而不是等到运行时才发现。
4. 为什么选择 tRPC?
- 绝对的端到端类型安全: 这是其核心价值。消除了因 API 契约不匹配而产生的一整类 Bug。
- 卓越的开发体验: IDE 的自动补全让你无需查阅文档或猜测 API 的结构。重构变得异常轻松和安全。
- 无需代码生成: 没有额外的构建步骤,反馈循环极快。
- 轻量且灵活: tRPC 本身非常小,并且可以与任何前端框架和后端服务集成。
结论
tRPC 为全栈 TypeScript 开发带来了前所未有的流畅体验。通过消除对 API 文档和 Schema 的依赖,它让前端和后端之间的协作变得无缝且极其安全。在 Monorepo 架构下,其优势被进一步放大。如果你正在构建一个全栈 TypeScript 应用,tRPC 绝对是值得你投入时间去尝试的革命性工具。