从 REST 的两个痛点说起
REST 足够好,所以它依然是默认选择;但有两个痛点在项目长大后会反复出现。第一是取数粒度错配:列表页只要三个字段,接口却把整个对象连附属数据一起吐回来(过度获取,over-fetching);详情页要的数据散在三个接口里,前端就得发三次请求再拼(欠获取,under-fetching)。第二是契约靠人肉维护:文档说字段叫 userName,后端某次重构改成了 nickname,前端上线才炸——类型漂移不是 bug,是缺一个机器强制的契约。
GraphQL 和 tRPC 是对这个问题的两条回答,思路完全不同:前者把”取什么”的权力交给客户端,用 schema 当机器可校验的契约;后者在 TypeScript 全栈的前提下,让类型直接从服务端代码推断到客户端,连 schema 文件都省掉。这篇把两者机制摆开对比,最后给场景化结论。
GraphQL:schema 即契约,客户端按需取
GraphQL(2015 年由 Facebook 开源,现由 GraphQL Foundation 维护)只有单一端点。服务端用 SDL(Schema Definition Language)声明有哪些类型和字段,比如订单类型 Order 带着 id、amount 和一个指向用户对象的 buyer 字段;客户端发一段 query 描述自己要哪些字段,服务端按字段逐个解析返回:
POST /graphql
{
"query": "query OrderList { orders(first: 20) { id amount buyer { name } } }",
"variables": {}
}
返回体里只有 query 点名的字段——列表面板只要 id、amount 和买家昵称,就一点不多给,这就是”按需取”。schema 是机器可校验的契约,GraphiQL 这类工具靠内省(introspection)就能自动补全并在字段写错时直接报错。
代价也在这套机制里,有三个绕不开的问题。其一,N+1:每个字段是一个 resolver(解析器),列表查询会逐行触发子字段查询,一百个订单就是一百次 buyer 查询;标准解法是 DataLoader 这类按请求批量合并缓存的工具,但它要求你理解并正确使用。其二,HTTP 缓存弱化:所有请求打同一个端点、多数客户端走 POST,URL 层的 GET 缓存作废,要靠持久化查询(persisted queries)等方案重新补缓存。其三,服务端要自己设防:客户端可以构造任意深度的嵌套查询,不做深度与复杂度限制,一条 query 就能把数据库打挂。学习曲线是三者里最陡的:schema 设计、resolver 组织、N+1、缓存与安全,四件事都得会。
tRPC:端到端类型推断,零 schema
tRPC 走的是另一条路:它不定义 schema,直接让客户端的类型从服务端函数的返回值推断过来。前提写死在名字里——TypeScript Remote Procedure Call,前后端都必须是 TS:
// 服务端:定义一个过程(procedure),输入用 zod 校验
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
orderList: publicProcedure
.input(z.object({ limit: z.number().max(50) })) // 校验规则即输入契约
.query(({ ctx, input }) => {
return ctx.db.order.findMany({ take: input.limit });
}),
});
// 客户端:input 的类型、返回值的类型,全部自动推断
const orders = trpc.orderList.useQuery({ limit: 20 });
// orders.data 完整可推导;把 orderList 改名,编译器立刻在所有调用点报错
没有 schema 文件、没有代码生成步骤、没有”文档与实现不同步”这回事——类型就是文档,而且永远是对的。配合校验器(zod 是主流选择)连输入类型都是双向强制。tRPC 与 TanStack Query 深度集成,客户端缓存、重试、失效这些心智可以直接复用。版本锚点:v11 于 2025 年 3 月发布稳定版,此后一直是当前大版本,以 11.x 持续迭代。
硬边界同样写在前提里:客户端必须是 TypeScript。App 原生端、小程序、第三方系统、任何非 TS 语言,一概接不进来——这些场景下 tRPC 直接出局,没有商量余地。
机制对比
| 维度 | REST | GraphQL | tRPC |
|---|---|---|---|
| 契约载体 | 文档/OpenAPI,人工维护 | SDL schema,服务端权威定义 | TS 类型自动推断,无独立契约 |
| 取数粒度 | 服务端定死 | 客户端 query 按需取 | 客户端选调哪个过程,粒度由过程设计定 |
| 类型安全路径 | 靠代码生成,常滞后于实现 | schema + 生成工具,较可靠 | 端到端原生,重命名即时全链报错 |
| N+1 风险 | 无(但欠获取要自己拼请求) | 默认踩坑,需 DataLoader | 无此机制性问题 |
| HTTP 缓存 | 天然友好(GET + URL) | 弱化,需持久化查询补课 | 走客户端缓存(React Query 体系) |
| 多语言客户端 | 都支持 | 都支持 | 仅限 TypeScript |
| 学习曲线 | 低 | 高(schema/N+1/缓存/安全四件套) | 中(需熟 TS 与 React Query) |
| 主要运维成本 | 接口数量增长 | 查询治理与安全限制 | 几乎为零,但锁死技术栈 |
场景推荐
| 场景 | 建议 | 理由 |
|---|---|---|
| 对外开放 API、第三方集成 | REST(或 GraphQL) | 受众最广,工具与文档生态最厚 |
| Web + iOS + 小程序的多端 BFF 层 | GraphQL | 一套 schema 各端按需取,聚合层价值最大 |
| TS 全栈的内部系统与中小产品 | tRPC | 类型直通迭代最快,无 schema 维护负担 |
| 前后端语言不同、或有 App/小程序端 | REST | GraphQL 太重,tRPC 前提不成立 |
| 多维筛选的分析类界面 | GraphQL | 字段组合灵活,不必为每种维度组合开接口 |
| 前后端同仓、人少活多的团队 | tRPC | 契约零维护成本,改接口即时见类型 |
反模式点名三处。其一,把 GraphQL 当 REST 用:每个业务发一个全字段的具名 query,客户端永远照单全收——”按需取”的权力没收了,schema 与 N+1 的运维成本却一分没少。其二,tRPC 项目把所有过程塞进一个 router 文件,类型推导链越来越长,编辑器提示慢到没法用;应按业务域拆 router 再合并。其三,GraphQL 上线不做查询深度和复杂度限制,把算力黑洞暴露给公网。
迁移成本与共存策略
两套方案都不要求一夜替换。GraphQL 的常见落地姿势是把它当 BFF(Backend for Frontend)聚合层:底层微服务保持 REST 不动,只在前端服务旁加一个 GraphQL 网关,把分散的接口聚合成一张 schema;客户端按页面逐步切换,新旧端点并存过渡。这个姿势的代价是多一个常驻网关要运维,收益是底层服务零改动——API 层在整体架构里的位置与职责划分,站内的微服务基础设施清单里有更完整的图景。
tRPC 的渐进路径更简单:在 Next.js 一类的全栈项目里,先拿纯内部模块(后台管理页、无外部调用方的功能)替换手写 fetch,公开端点继续 REST。因为同一仓库内类型即时生效,每替换一个过程都能立刻编译验证,风险半径很小。要警惕的是”半个项目 tRPC、半个项目裸 fetch”长期混跑且没有边界说明——三个月后没人记得哪个模块走哪条路;要么按模块整块切,要么在目录约定里写清楚。
还有两处差异值得在选型时过一遍。错误处理:REST 的语义挂在 HTTP 状态码上,GraphQL 把错误放在响应的 errors 数组里并允许部分成功(部分字段报错、其余照常返回),两者跨语言通用;tRPC 的错误形状由 TS 类型描述,TS 客户端里处理得很顺,出了 TS 世界就无人能读——这也是对外端点保持 REST 的又一个理由。实时数据:GraphQL 有内建的 Subscription 机制(通常走 WebSocket),tRPC 没有对应的订阅模型,实时需求要自己搭 WebSocket 通道。
结论与边界
诚实结论:多数中小团队的真实答案仍是”REST + 一个好的客户端请求层”。GraphQL 的成本在服务端——N+1、缓存、查询治理,收益在多端取数;tRPC 的成本在生态锁定——TS only,收益是端到端类型直通;两者都不是”更先进的 REST”,而是不同场景的专用工具。另外别高估方案本身:无论选哪条路,客户端缓存的心智(什么数据该缓存、什么时候失效)都决定实际体验,这层值得单独学。
延伸阅读:客户端缓存与请求管理见《TanStack Query 基础》;同类”类型驱动 headless”的设计风格见《TanStack Table 基础》;Next.js 里数据获取的框架侧接入见《Next.js 数据获取基础》;API 层在整体架构中的位置见《微服务基础设施清单(注册中心/网关/链路追踪)》。
版本核实(以官方文档为准):tRPC v11 于 2025-03-21 发布稳定版并沿用至今(11.x 系列持续迭代),文中示例按 v11 API 书写;GraphQL 规范由 GraphQL Foundation 维护、持续演进无”大版本”概念;TanStack Query 按 v5 稳定线描述。细节以 trpc.io、graphql.org 官方文档为准。