tRPC
tRPC 是一个基于 TypeScript 的端到端类型安全 RPC 框架。客户端像调用本地函数一样调用服务端过程,类型从服务端代码直接推导到客户端,无需 codegen、无需 Schema 文件。
Note
特点
- 端到端类型安全:服务端修改接口,客户端立即得到类型提示
- 客户端像调用本地函数一样调用远程过程,不定义 HTTP 方法和路径
- 同一页面多个 tRPC 请求自动合并为一个 HTTP 请求(batching)
- 与 TanStack Query 深度集成,开箱即用缓存、乐观更新、无限滚动
Note
局限
- 仅限 TypeScript:前后端必须都是 TS,无法服务非 TS 客户端
- 前后端强耦合:类型变更直接传播,适合同一团队,不适合对外 API
- 生态较小:相比 REST / GraphQL,社区工具、中间件有限
- 调试不友好:默认 SuperJSON 序列化 + batching 合并响应,DevTools Network 面板基本不可读
Note
适用场景
- TypeScript 全栈项目、前后端同团队或 monorepo
- 追求最快开发速度和零配置类型安全
- 不需要对外暴露 API 给第三方
REST vs tRPC 的调用形态
┏━━━━━━━━ REST API Client ━━━━━┓ ┏━━━━━━━━━━━━ REST API Server ━━━━━━━━━━━━━━┓
┃ ┃ ┃ ┃
┃ axios.get("/api/user") ━━━━ GET ━━━▶ /api/user ━━▶ getUserList ┃
┃ axios.get("/api/user/1") ━━━━ GET ━━━▶ /api/user/1 ━━▶ getUserById ┃
┃ axios.post("/api/user") ━━━━ POST ━━━▶ /api/user ━━▶ createUser ┃
┃ axios.put("/api/user/1") ━━━━ PUT ━━━▶ /api/user/1 ━━▶ updateUserById ┃
┃ axios.delete("/api/user/1") ━━━ DELETE ━━━▶ /api/user/1 ━━▶ deleteUserById ┃
┃ ┃ ┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━ tRPC Client ━━━━━━━━┓ ┏━━━━━━━━━━━━━ tRPC Server ━━━━━━━━━━━━━━━━━┓
┃ ┃ ┃ ┃
┃ trpc.getUserList.query() ━━━━━━━━━━━━▶ getUserList ┃
┃ trpc.getUserById.query() ━━━━━━━━━━━━▶ getUserById ┃
┃ trpc.createUser.mutate() ━━━━━━━━━━━━▶ createUser ┃
┃ trpc.updateUserById.mutate()━━━━━━━━━━━▶ updateUserById ┃
┃ trpc.deleteUserById.mutate()━━━━━━━━━━━▶ deleteUserById ┃
┃ ┃ ┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
基础概念
| 概念 | 一句话说明 | 详细 |
|---|---|---|
| Router(路由器) | 组织和注册所有 API 端点的容器 | 详见 |
| Procedure(过程) | 单个 API 端点,分为 query(读)和 mutation(写) | 详见 |
| Context(上下文) | 每个请求共享的数据(数据库连接、session 等) | 详见 |
| Middleware(中间件) | 在 procedure 执行前后插入逻辑(鉴权、日志) | 详见 |
| Client(客户端对象) | 客户端调用服务端 procedure 的类型安全代理 | 详见 |
类型推导原理
tRPC 不需要 codegen、不需要 Schema 文件——类型直接从服务端代码推导到客户端。
GraphQL: Schema (.graphql) → codegen → 生成 TS 类型 → 客户端使用
tRPC: 服务端代码 → TypeScript 编译器直接推导 → 客户端自动获得类型
核心机制:
// 服务端:定义 procedure
const appRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
});
// 只导出类型,不导出运行时代码
export type AppRouter = typeof appRouter;
// 客户端:import 类型
import type { AppRouter } from "../server/router";
const trpc = createTRPCClient<AppRouter>({
/* ... */
});
// TS 自动推导:
// - trpc.getUser.query({ id: "1" }) 的输入必须是 { id: string }
// - 返回值类型自动是 User | null
// - 服务端改了字段名,客户端立即报类型错误
Note
要点:为什么不需要 codegen
- 利用 TypeScript 的
typeof+ 泛型推导——AppRouter类型包含了所有 procedure 的输入/输出 import type只在编译时存在,不会打包到客户端 JS- 前提:前后端必须在同一个 TypeScript 项目(monorepo),否则类型无法传递
下载安装
% npm install @trpc/server @trpc/client zod
客户端如果用 React + TanStack Query:
% npm install @trpc/react-query @tanstack/react-query
基本使用
基本流程:
- 服务端:初始化
t实例 → 定义 Router 和 Procedure → 导出AppRouter类型 - 服务端:挂载 HTTP handler(Fetch adapter / Next.js Route Handler / Express middleware 等)
- 客户端:用
createTRPCClient<AppRouter>()或createTRPCReact<AppRouter>()创建代理 - 客户端:调用
.query()/.mutate(),享受类型推导
最小可跑示例
服务端:
// server.ts
import { initTRPC } from "@trpc/server";
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { z } from "zod";
const t = initTRPC.create();
const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello ${input.name}`),
});
export type AppRouter = typeof appRouter;
createHTTPServer({ router: appRouter }).listen(3000);
客户端:
// client.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server";
const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: "http://localhost:3000" })],
});
const greeting = await trpc.hello.query({ name: "Alice" });
// greeting 自动推导为 string;传 { name: 1 } 会编译报错
完整 Router / Context / Middleware / React 集成见 Router 与 Procedure。
与 GraphQL 对比
完整对比表与判断流程见 GraphQL 总览 — GraphQL vs tRPC。
一句话判断原则:前端只有 TS 客户端 + 同团队 → tRPC;否则 GraphQL。