两种 API 范式解决的其实不是同一个问题——理解它们的分歧点,比记住语法差异更重要。
每当团队要为新项目选型 API 风格时,"REST 还是 GraphQL" 几乎是绕不开的争论。很多讨论止步于语法层面的对比——"REST 用多个端点,GraphQL 用一个端点"——但这掩盖了两者更本质的分歧:它们对"谁来决定返回什么数据"这件事,给出了完全相反的答案。
这篇文章会从设计哲学出发,逐层拆解两者在数据获取、类型系统、版本管理、缓存、性能和生态系统上的差异,并给出一个相对务实的选型框架。
REST(Representational State Transfer)诞生于 2000 年 Roy Fielding 的博士论文,核心思想是把系统建模成"资源"的集合,每个资源有唯一的 URI,客户端通过标准的 HTTP 动词(GET / POST / PUT / DELETE)对资源进行操作。服务端决定每个端点返回什么形状的数据。
GraphQL 由 Facebook 在 2012 年内部孵化、2015 年开源,出发点是移动端在弱网环境下对"精确控制传输数据量"的强烈需求。它把 API 建模成一张类型图(Schema),客户端在请求中声明自己需要哪些字段,服务端按需返回。
图 1:REST 中数据形状由服务端固定;GraphQL 中数据形状由客户端在查询中声明
这个分歧解释了后面几乎所有的具体差异:谁掌握"形状"的决定权,谁就承担相应的复杂度。REST 把复杂度留在了"如何设计出恰到好处的端点"上;GraphQL 把复杂度转移到了"如何设计一个既灵活又安全高效的 Schema 与解析层"上。
REST 最常被诟病的两个问题是过度获取(over-fetching)和获取不足(under-fetching)。前者是端点返回了比客户端需要更多的字段;后者是客户端需要多次请求才能拼出完整的视图——比如先 GET /user/1 拿到用户信息,再用返回的 id 去 GET /user/1/posts,如果还要评论数据,可能还得再发一次请求,形成"请求瀑布"。
GraphQL 通过单一端点和声明式查询天然解决了这两个问题:一次请求即可精确取回嵌套的关联数据。但这不是没有代价的——服务端需要一个能高效解析任意查询形状的执行引擎,稍不注意就会在解析嵌套字段时触发经典的 N+1 查询问题(例如查询 100 篇文章的作者,如果没有做批处理,会对数据库发起 101 次查询)。业界通常用 DataLoader 之类的批处理/缓存层来缓解这个问题,但这本身就是 GraphQL 服务端需要额外掌握的一层复杂度。
REST 请求瀑布示例:
# 1. 获取用户
GET /api/users/1
# 2. 用返回的 id 获取文章列表
GET /api/users/1/posts
# 3. 对每篇文章再获取评论数(N 次请求)
GET /api/posts/101/comments
GET /api/posts/102/comments
# ...
GraphQL 单次声明式查询:
query {
user(id: "1") {
name
posts {
title
comments {
author
text
}
}
}
}
GraphQL 的 Schema 本身就是强类型契约,配合工具链(如 GraphQL Code Generator)可以自动生成前端类型定义,减少前后端沟通成本,并让 IDE 具备字段级别的自动补全和查询校验能力。
REST 本身不强制类型系统,但生态里有 OpenAPI(Swagger)规范来弥补——通过一份 YAML/JSON 描述文件定义接口、参数和响应结构,同样可以生成客户端 SDK 和文档。区别在于:OpenAPI 是"外挂"的约定,需要额外维护并容易与实现脱节;GraphQL 的 Schema 则是运行时自省(introspection)的一部分,类型定义与实际执行逻辑天然同步。
REST:通常通过 URI 或请求头显式做版本管理,例如 /api/v1/users 和 /api/v2/users 并存。这种方式直观,但长期会积累多套并行维护的端点。
GraphQL:官方推荐"演进式 Schema"而非显式版本号:新增字段不影响旧查询,废弃字段用 @deprecated 标记并给出迁移期,而不是直接切断。这依赖持续的 Schema 治理纪律,否则 Schema 会随时间膨胀、积累大量僵尸字段。
这是 REST 相对 GraphQL 的一个显著优势。REST 天然借助 HTTP 语义——GET 请求可以被浏览器、CDN、反向代理在 URL 层面缓存,ETag、Cache-Control 等标准头部开箱即用。
GraphQL 所有请求通常都走 POST 到同一个端点,URL 本身不再携带查询语义,标准 HTTP 缓存机制基本失效。客户端库(如 Apollo Client、Relay)转而在应用层维护基于对象 ID 的规范化缓存,服务端也常引入持久化查询(Persisted Queries)等方案来部分找回缓存能力,但整体复杂度明显更高。
GraphQL 赋予客户端构造任意深度查询的自由,这也意味着一个恶意或不小心写出的深度嵌套/循环引用查询可能让服务端付出巨大的计算与数据库开销(所谓"查询炸弹")。因此生产环境的 GraphQL 服务通常需要额外实现查询深度限制、查询复杂度评分、请求速率限制等防护措施,这些在 REST 里因为端点和返回形状都是预先定义好的,天然就不存在。
提醒:如果团队决定采用 GraphQL,查询复杂度限制和深度限制不是"可选的加分项",而应该在上线第一天就配置好——这是很多团队踩过的坑。
| 维度 | REST | GraphQL |
|---|---|---|
| 数据获取精确度 | 较弱,易过度/不足获取 | 强,客户端按需声明 |
| HTTP 缓存 | 原生支持 | 需应用层方案 |
| 类型系统 | 需 OpenAPI 外挂 | 内建 Schema |
| 版本管理 | 显式版本号 | 演进式 Schema,需治理 |
| 学习曲线 | 低,生态成熟 | 中,需理解 Schema/Resolver |
| 文件上传/流式传输 | 原生支持 | 需扩展规范 |
| 服务端复杂度 | 较低 | 较高(N+1、深度限制等) |
| 多端/移动端适配 | 常需 BFF 层 | 天然适配不同客户端需求 |
一个务实的现实是:很多大型系统并非"二选一",而是在同一个后端上,对外的公开 API 保留 REST,内部面向前端聚合的层用 GraphQL 作为 BFF(Backend For Frontend),两者各自发挥所长。
REST 和 GraphQL 不是"新旧替代"关系,而是两种针对不同约束条件做出的设计权衡。REST 把简单性和缓存友好性放在优先位置,代价是数据获取的灵活性;GraphQL 把数据获取的精确性和客户端自由度放在优先位置,代价是服务端实现和运维的复杂度。理解这个权衡背后的动机,比记住某个语法细节更能帮你在真实项目中做出合适的选择。