GraphQL 与 tRPC:API 风格怎么选

从 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 官方文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇