tRPC 端到端类型安全:无需 API 文档
tRPC 端到端类型安全:无需 API 文档
欢迎来到这个免费在线教程,你将学会如何使用 tRPC 构建从数据库到 UI 的完整类型安全应用,真正告别手写 API 文档和手工类型声明。
什么是 tRPC?
tRPC (TypeScript Remote Procedure Call) 是一个全栈 TypeScript 框架,让你能定义一个过程(procedure)并直接在客户端调用它,就像调用本地函数一样。它的核心承诺:如果你修改了服务端代码,客户端不匹配的地方会立刻报 TypeScript 编译错误。
比较熟悉的场景:
- 传统 REST API:你需要手动保持服务端路由、参数、返回值的类型和客户端请求代码的一致性,出问题只在运行时暴露。
- tRPC:编译器帮你检查一切,无需任何代码生成,完全依靠 TypeScript 的类型推断。
为什么“端到端类型安全”如此重要?
传统开发中的“类型断层”
一个典型 Web 应用通常有这几个层次:
数据库 ↔ 后端逻辑 ↔ API 路由 ↔ HTTP ↔ 客户端请求 ↔ UI 组件
多数方案只能保证某一层的类型安全,比如 Prisma 保证数据库操作的类型,REST + axios 则必须手动声明接口响应类型。任何一处的改动都可能造成运行时空错误,而编译阶段完全不知道。
tRPC 如何消除断层
tRPC 让你在客户端直接导入服务端的类型定义——不是重新编写,而是自动推导出类型。架构变成:
服务端 Procedures → 导出 AppRouter 类型 → 客户端通过 infer 生成强类型 hooks
这样就形成了一条没有缝隙的类型链:数据库模型类型 → 过程输入/输出类型 → 客户端调用类型 → UI props 类型。修改任何一环,整个链条的编译都会立刻给出错误提示。
核心概念快速预览
它们是什么?
- Router(路由):组织 procedures 的容器,类似于传统 API 的域名空间。
- Procedure(过程):一个可以被远程调用的函数,有
query(读)、mutation(写)、subscription(实时)三种。 - Context(上下文):在每个请求中注入数据库连接、用户认证等对象。
- Middleware(中间件):在过程执行前后运行的逻辑,用于鉴权、日志等。
与 REST 和 GraphQL 的思维差异
| REST | GraphQL | tRPC | |
|---|---|---|---|
| 接口定义 | URL + HTTP 方法 + 状态码 | Schema 语言 | TypeScript 函数签名 |
| 类型来源 | 手动编写或生成 | 从 Schema 生成 | 直接从代码推导 |
| 工具需求 | Swagger/Postman 文档 | GraphiQL/Playground | 仅依靠 IDE 自动补全 |
| 耦合度 | 松耦合,但易产生类型不一致 | 强类型但需单独维护 Schema | 强耦合,类型即时同步 |
从零搭建的第一个 tRPC 应用
我们将创建一个简单的“待办事项”应用,后端使用纯 Node.js + Express,前端使用 React。
1. 项目初始化
mkdir trpc-todo-app
cd trpc-todo-app
# 后端目录
mkdir server && cd server
npm init -y
npm install @trpc/server @trpc/client zod express cors
npm install -D typescript @types/express ts-node nodemon
npx tsc --init
cd ..
# 前端目录
npx create-react-app client --template typescript
cd client
npm install @trpc/client @trpc/server @trpc/react-query @tanstack/react-query zod
cd ..
在 server/tsconfig.json 启用严格模式和路径映射,便于类型导出。
2. 定义服务端过程
在 server/src 下创建 trpc.ts、router.ts 和 index.ts。
trpc.ts – 初始化 tRPC 实例
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
// 上下文类型(稍后注入)
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
router.ts – 定义待办事项路由
import { router, publicProcedure } from './trpc';
import { z } from 'zod';
type Todo = {
id: string;
content: string;
done: boolean;
};
const todos: Todo[] = [];
export const appRouter = router({
list: publicProcedure.query(() => {
return todos;
}),
add: publicProcedure
.input(z.object({ content: z.string() }))
.mutation(({ input }) => {
const todo: Todo = {
id: `${Date.now()}`,
content: input.content,
done: false,
};
todos.push(todo);
return todo;
}),
});
// 导出路由器类型,这是类型安全的秘密!
export type AppRouter = typeof appRouter;
关键点:
export type AppRouter = typeof appRouter;这一行就是客户端获取全部类型信息的源头。你完全不需要额外编写接口文档。
3. 构建 Express 服务器并启用 tRPC 中间件
index.ts
import express from 'express';
import cors from 'cors';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { appRouter } from './router';
const app = express();
app.use(cors());
app.use(
'/trpc',
createExpressMiddleware({
router: appRouter,
createContext: () => ({}),
})
);
app.listen(4000, () => {
console.log('Server running on port 4000');
});
运行服务端:
npx ts-node src/index.ts
4. 在前端安全地消费 API
在 React 项目中创建一个 tRPC 客户端工厂 client/src/trpc.ts:
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/src/router'; // 直接引入类型
export const trpc = createTRPCReact<AppRouter>();
设置 Provider 和配置 client/src/App.tsx:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from './trpc';
import { useState } from 'react';
import TodoList from './TodoList';
const queryClient = new QueryClient();
function App() {
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:4000/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
</trpc.Provider>
);
}
5. 使用完全类型化的 Hooks 构建 UI
在 TodoList.tsx 中:
import { trpc } from './trpc';
import { useState } from 'react';
export default function TodoList() {
const utils = trpc.useContext();
const listQuery = trpc.list.useQuery();
const addMutation = trpc.add.useMutation({
onSuccess: () => {
utils.list.invalidate(); // 添加成功后刷新列表
},
});
const [text, setText] = useState('');
return (
<div>
<h2>我的待办</h2>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button
onClick={() => {
addMutation.mutate({ content: text });
setText('');
}}
>
添加
</button>
{listQuery.data?.map((todo) => (
<div key={todo.id}>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.content}
</span>
</div>
))}
</div>
);
}
注意:
trpc.list.useQuery()自动返回{ data: Todo[] }类型,data下的每个属性都有智能提示。addMutation.mutate需要的参数类型被input定义的 Zod schema 严格约束,传入错误的属性会即时报错。
6. 感受“修改一处,处处检查”
试着将服务端 add procedure 的输入 schema 改为需要 content 和 priority:
add: publicProcedure
.input(z.object({ content: z.string(), priority: z.number() }))
.mutation(...)
保存后,前端 addMutation.mutate({ content: text }) 会立刻提示缺少 priority 属性。这就是无需文档的端到端类型安全。
进阶技巧:类型安全的上下文注入
上例中上下文为空,真实项目需要注入数据库客户端。例如使用 Prisma:
// server/src/context.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export type Context = { prisma: PrismaClient };
// 在 trpc.ts 中传递类型
import { initTRPC } from '@trpc/server';
import type { Context } from './context';
export const t = initTRPC.context<Context>().create();
现在所有过程都能通过 ctx 访问类型安全的数据库操作,而前端对此无感知却始终能获得正确类型。
常见问题解答
Q: tRPC 只适用于全栈 TypeScript 吗?
基本上是的。tRPC 的价值建立在服务端和客户端都能解析同一份类型上。如果后端使用其他语言,可以考虑用 gRPC 搭配类型生成,但成本更高。
Q: 传统 REST 接口还能用吗?
可以,但就失去了端到端类型安全。你可以在 tRPC 之上再暴露 REST 端点,但建议全站迁移到 tRPC。
Q: GraphQL 也有类型,tRPC 有什么不同?
GraphQL 的类型系统(Schema)独立于实现代码,需要额外工具同步。tRPC 的类型自动从实现代码推导,无需 Code Gen,迭代速度更快。
Q: 生产环境部署有什么注意事项?
- 使用
ts-node启动仅适合开发,生产应编译成 JS 运行。 - 用 Next.js、Nuxt 等框架可以直接集成 API 路由,避免单独部署。
- tRPC 本身无状态,可轻松水平扩展。
总结:从这里走向生产
通过本教程,你掌握了:
- 定义带 Zod 校验的过程
- 导出 AppRouter 类型并跨前端使用
- 用 React Query hooks 驱动 UI,享受自动补全和编译期错误检查
下一步行动:
- 克隆官方示例仓库
trpc/examples-next-prisma-starter体验完整数据库集成。 - 学习中间件和订阅功能构建实时应用。
- 将你现有项目的一个模块迁移到 tRPC,直观感受类型安全的效率提升。
当你再也无需反复检查接口参数名称和返回格式时,你就真正融入了端到端类型安全的开发节奏——tRPC 将类型变成了你的文档,而且永远不会过期。