告别 API 文档:使用 tRPC 构建端到端类型安全的应用

5 min

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 绝对是值得你投入时间去尝试的革命性工具。