#TypeScript#DevOps

API 设计:REST、GraphQL、gRPC 怎么选

选错 API 风格,后期改不动。本文讲清 REST、GraphQL、gRPC 各自的哲学与适用场景:REST(资源/通用/简单)、GraphQL(灵活取数/前端驱动)、gRPC(高性能/内部微服务),按场景选型,以及 REST 自己也有坑(版本/状态码/嵌套)。

本文要点

  • 主题聚焦:TypeScript、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年6月20日·更新于 2026年7月23日

API 风格这件事,网上讨论十有八九会变成"REST vs GraphQL vs gRPC 谁更强"的站队。这种讨论没意义。这三种风格不是同一道题的三个答案,而是为不同场景设计的三套哲学——选错一套,代价不是"性能差一点",而是后期改不动:字段加不动、客户端崩、内部服务调用绕一层。

这篇文章不讲谁胜出,讲怎么按场景选型,以及每种风格自己要踩的坑。

一、三种风格的哲学差异:资源、查询、方法

理解三者的差异,先看它们各自把"API 是什么"这件事定义成什么。

  • REST:资源导向。整个 API 是一组"名词"(资源),用 HTTP 方法(GET/POST/PUT/DELETE)对这些名词做动作。本质是"对一组资源的状态做转移"( Representational State Transfer)。
  • GraphQL:查询驱动。客户端写一段查询,描述"我要什么",服务端按这段查询返回。本质是"对一张图(Graph)做查询"。
  • gRPC:方法调用。本质是远程过程调用(RPC),你像调本地函数一样调远端方法,输入输出都是强类型消息。

一张对比表先放这儿,后面逐个展开:

维度RESTGraphQLgRPC
抽象模型资源(名词)查询(Graph)方法调用
协议HTTP/1.1 或 HTTP/2HTTP(通常是 POST)HTTP/2
数据格式JSON(主流)JSONProtobuf(二进制)
取数粒度端点固定客户端按需消息字段固定
浏览器友好极好好差(要 gRPC-Web 或网关)
缓存HTTP 缓存原生支持难(POST 无缓存语义)无
典型场景公开 API、CRUD前端聚合、多变 UI内部微服务、低延迟

记住这张表,基本就记住了三者各自的位置。

二、REST:成熟、通用,但坑也最多

REST 的优势是显而易见的:HTTP 原生。它直接复用 HTTP 的语义——方法、状态码、Header、缓存。这意味着 CDN、反向代理、浏览器缓存、curl、Postman 这些工具你都能白嫖。对外公开 API 几乎默认就是 REST,因为生态最厚、学习成本最低。

适合的场景很明确:公开 API、第三方集成、CRUD 为主的业务。一个订单系统,创建订单、查询订单、改状态、删除——这种业务用 REST 写出来干净得像教科书。

但 REST 的坑也是真的多,而且都是"设计期不觉得,运行期才发现"的坑:

  1. 版本怎么管。一旦上线,改字段就是破坏性变更。常见做法是 URL 里带版本(/api/v1/orders),也有用 Header 的(Accept: application/vnd.myapi.v1+json)。两种都能用,关键是一开始就想好,别等 v3 才想起来。
  2. 状态码语义被滥用。最典型的是 HTTP 200 包一个 { code: 500, msg: "失败" }。这等于把 HTTP 状态码完全架空。正确做法是让 HTTP 状态码有意义:201 Created、400 Bad Request、404 Not Found、422 Unprocessable Entity、429 Too Many Requests,业务错误细节放 body。
  3. 过度获取 / 不足获取(over-fetching / under-fetching)。列表接口返回了 30 个字段,前端只用 3 个——这是过度获取;要看订单详情又要调 5 个接口——这是不足获取。这是 REST 被 GraphQL 吐槽最多的痛点。
  4. 嵌套资源怎么设计。/users/123/orders 还是 /orders?userId=123?没有标准答案,但两层以内嵌套是常见的可接受上限,再深就该用查询参数或独立端点。

三、GraphQL:灵活是它的优点,也是它的负担

GraphQL 解决的核心问题是上一节说的"过度/不足获取"。前端自己写查询,要什么字段取什么字段,一次请求可以把多个资源一起拿回来。再加一个强类型 Schema,前端能拿到精确的类型定义做自动补全。

graphql
query {
  user(id: "123") {
    name
    orders(last: 3) {
      total
      items { name price }
    }
  }
}

一次请求拿到用户、订单、订单项三个资源,字段精确到不能再少。这种体验对前端多变、需要聚合多源数据的场景(典型:电商首页、内容平台、Dashboard)是降维打击。

但 GraphQL 的代价同样真实:

  1. N+1 问题。查一个 list 的用户,每个用户又关联了订单,naive 实现会发出 N 次订单查询。必须上 DataLoader 做批量加载,这是额外的心智负担。
  2. 复杂查询攻击。恶意客户端可以构造深度嵌套的查询把服务端打挂。必须做查询深度限制和复杂度评分。
  3. 缓存难。GraphQL 走 POST,而 POST 没有 HTTP 缓存语义。要靠客户端缓存(Apollo Client、Relay)和服务端的持久化查询(persisted queries)来补。
  4. 服务端实现成本高。Schema 设计、resolver 编排、权限粒度(每个字段都要考虑授权)——比写一个 REST controller 复杂得多。

别为了"前端少调几次接口"就上 GraphQL。如果接口数量稳定、前端场景单一,REST 完全够用,GraphQL 反而是负担。

四、gRPC:内部服务的性能王者

gRPC 走的是另一条路。它用 Protobuf 做二进制序列化(比 JSON 小 30%-70%,解析快一个数量级),跑在 HTTP/2 上(多路复用、头部压缩),原生支持双向流(streaming)。接口契约通过 .proto 文件定义,生成多语言客户端。

protobuf
service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc SubscribeOrders(SubscribeRequest) returns (stream OrderEvent);
}

这种组合让 gRPC 在内部微服务通信、低延迟场景里几乎没有对手:服务网格、实时数据流、高频交易、内部 RPC 网关。微服务之间互相调,延迟从几十毫秒压到个位数毫秒,gRPC 是关键。

gRPC 的流式能力特别值得提一句。四种模式:一元调用(unary)、服务端流、客户端流、双向流。订阅实时行情、日志上报、聊天室消息推送——这些用 REST 要么轮询、要么上 WebSocket,而 gRPC 的双向流是原生支持,且类型安全。

但 gRPC 不适合直接对外。浏览器对 HTTP/2 的 trailer、对 Protobuf 的处理都不友好,通常需要 gRPC-Web 或者一个 gRPC-Gateway 把它转成 REST/JSON 暴露出去。调试也比 REST 难——你没法直接 curl 一个二进制 body,通常要靠反射(reflection)配合命令行工具比如 grpcurl 才能调通。

五、选型决策:别站队,看场景

把上面的判断收敛成一句话:看这条 API 面向谁。

  • 面向外部、第三方、浏览器 → REST。生态厚、可缓存、易调试,公开 API 的默认选择。
  • 面向自家前端、UI 多变、需要聚合多源 → GraphQL。前端按需取数,后端少接几次 ad-hoc 需求。
  • 面向内部服务间通信 → gRPC。性能、流、强类型契约。

实战里这三种经常组合用。一个相当常见的架构是:

text
浏览器 ──REST/GraphQL──> BFF(Backend for Frontend) ──gRPC──> 内部微服务

对外用 REST 或 GraphQL 保证友好,对内用 gRPC 保证性能。这不是骑墙,这是分层:每一层用最合适的工具。

六、REST 最佳实践:做对细节

如果选了 REST(大多数对外 API 的选择),下面这些细节做对了,API 质量就超过了八成的同行:

  1. 资源用名词复数:/orders 而不是 /getOrder、/createOrder。动作交给 HTTP 方法。
  2. 版本放 URL 里:/api/v1/orders。简单、显式、好调试,比 Header 版本更通用。
  3. 状态码语义化:201 创建成功、204 无内容、400 请求格式错、422 业务校验失败、401 未认证、403 无权限、429 限流、500 服务端错。别全返回 200。
  4. 分页用游标(cursor)而非 offset:offset 在数据变动时会错位(漏数据或重复数据)、在大表上慢到爆炸(OFFSET 100000 要扫 10 万行)。游标分页稳定且快,而且天然支持"增量拉取"。返回结构带上 nextCursor,前端下次带上即可。
  5. 过滤、排序走查询参数:/orders?status=paid&sort=-createdAt。把表达力交给查询参数,而不是设计 /getPaidOrdersSortedByDate 这种 RPC 式端点。
  6. 错误格式统一:无论哪个端点出错,返回结构一致。
json
{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "订单 123 不存在",
    "details": { "orderId": "123" }
  }
}

七、API 文档不是可选项

最后一点,经常被忽略:API 没文档等于没设计。

  • REST 用 OpenAPI(Swagger)——.yaml 描述每个端点、参数、响应,生成可交互文档和客户端 SDK。
  • GraphQL 的 Schema 本身就是文档,配上 GraphiQL / Apollo Studio 自动可视化。
  • gRPC 的 .proto 文件就是契约,生成多语言 stub 的同时也能生成文档。

文档不是写给文档工具看的,是写给调用方和三个月后的自己看的。一份能跑的 OpenAPI,能让前端独立于后端推进,能让 SDK 自动生成,能让契约测试落地。

小结

  • REST 适合对外:HTTP 原生、缓存友好、生态最厚;坑在版本、状态码语义、过度/不足获取。
  • GraphQL 适合前端多变场景:按需取数、强类型 Schema、一次聚合多源;代价是 N+1、复杂查询防护、缓存难。
  • gRPC 适合内部服务:Protobuf + HTTP/2 + 流,低延迟、强契约;不适合直接对外,要 Gateway。
  • 三者常组合:对外 REST/GraphQL,内部 gRPC,中间用 BFF 衔接。
  • 选型标准只有一个:看这条 API 面向谁,而不是哪种风格更"先进"。

金句:好的 API 设计不在于选了哪种风格,而在于每一层都用了最合适的工具,并且把契约(文档/Schema/proto)当成一等公民对待——风格可以争论,契约不能省。

Related

相关推荐