2012年的Facebook有一个典型的工程困境:他们的iOS应用为了渲染用户动态消息流(News Feed),需要在一个屏幕上展示用户头像、昵称、帖子内容、图片缩略图、点赞数、评论列表——包括每条评论的作者头像和名字。按照REST API的设计,这需要客户端发起一组链式请求:
GET /users/me — 获取当前用户基本信息GET /users/me/feed?limit=20 — 获取帖子ID列表GET /posts/{id} × 20 — 逐个获取帖子详情GET /posts/{id}/comments?limit=3 × 20 — 获取每篇帖子的评论GET /users/{id} × N — 获取评论者的头像和昵称五个层级,加起来可能超过60个HTTP请求。在3G网络下,每个请求平均200-300ms延迟,整个页面加载需要等十几秒。更要命的是,很多请求返回了客户端根本不需要的字段——用户的教育背景、注册日期、帖子全文(列表页只显示摘要),这些多余的数据白白消耗了用户流量。
Facebook的工程师Lee Byron后来说过,他们当时的内部API有超过1000个端点,不同团队各自为政,前端要拼凑数据就像在不同部门之间跑腿递文件。这不是REST本身的错——REST在它诞生的Web 1.0和2.0时代确实运行良好——但在移动互联网和单页应用的新语境下,"一个URL对应一个资源"的假设已经不成立了。
问题可以归结为两个技术现象:
过度获取(Over-fetching):客户端调用 /users/123 获取用户名和头像,但服务器返回了包含地址、电话、工作经历等几十个字段的完整用户对象。如果这个API服务于三种不同客户端——Web端需要完整资料、移动端只需要姓名头像、后台管理端需要全部字段——那每个端点要么返回所有字段让移动端浪费带宽,要么为每种客户端维护不同端点导致端点爆炸。
获取不足(Under-fetching):单个端点返回的数据不够渲染一个完整视图,客户端需要额外发起请求来补全。这在REST中几乎是不可避免的,因为REST把数据打散到不同资源端点,而真实UI需要的数据往往是跨资源的聚合视图。
这两个问题不是设计失误能解决的——它们是REST"资源导向"模型的固有代价。
GraphQL的核心思路转换是:不再让服务器决定数据的粒度和形态,而是让客户端精确声明自己想要什么。这听起来像是"把球踢给前端",但实际上它是在前端和后端之间引入了一个基于Schema的中间契约。
在GraphQL的世界里,没有几十个URL端点,只有一个 /graphql 入口。客户端发送一段结构化的查询文本,这段文本的形状就是期望返回的JSON的形状:
query { user(id: "1001") { name avatarUrl posts(last: 3) { title createdAt likesCount } } }
服务器收到这个查询后,执行引擎会把它解析成抽象语法树(AST),验证查询是否合法,然后逐字段调用对应的解析器函数(Resolver)从后端数据源取数,最终组装成一个与查询结构同构的JSON响应:
{ "data": { "user": { "name": "林远", "avatarUrl": "https://cdn.example.com/avatars/1001.jpg", "posts": [ { "title": "GraphQL实战笔记", "createdAt": "2025-12-01", "likesCount": 42 }, { "title": "微服务API网关设计", "createdAt": "2025-11-28", "likesCount": 17 }, { "title": "从REST到GraphQL的迁移路径", "createdAt": "2025-11-15", "likesCount": 89 } ] } } }
一个请求,一次往返,不多不少。如果移动端只想要名字和帖子标题,查询里就不写 avatarUrl、createdAt、likesCount,服务器就不会返回这些字段。
从架构角度看,REST和GraphQL代表了两种不同的数据组织哲学。REST把世界看作资源集合——每个URL标识一个资源,HTTP动词定义操作方式。这像是一座图书馆,每本书有固定的位置,你按照索引去找。
GraphQL把世界看作数据图谱——数据是节点(对象)和边(关系)组成的网络,客户端可以沿着关系路径遍历这张图,精确选择沿途需要的字段。这更像是一张思维导图,你可以从任何节点出发,按需展开。
REST的每个端点是独立的,客户端需要知道有哪些端点、参数是什么、返回格式如何——这些信息通常散落在文档里。GraphQL只有一个端点,API的完整能力描述在Schema里,Schema是机器可读的,客户端可以通过内省(Introspection)查询自动获取。
在REST中,如果API需要演进——比如把 fullName 字段拆成 firstName 和 lastName——常见的做法是发布 v2 版本(/v2/users/123),然后维护两套代码。在GraphQL中,新增字段是向后兼容的(现有查询不受影响),废弃字段用 @deprecated 指令标记,给客户端足够长的迁移窗口。

GraphQL的所有查询能力都建立在一个预先定义的Schema上。Schema使用SDL(Schema Definition Language)编写,它描述了API能提供的所有类型、字段和关系。这是GraphQL与REST最根本的差异之一——REST的API契约通常是外部文档(OpenAPI/Swagger),而GraphQL的契约就是Schema本身,它是机器可验证的、内省可查的。
一个简单的博客Schema:
type Query { user(id: ID!): User post(id: ID!): Post feed(limit: Int, offset: Int): [Post!]! } type User { id: ID! name: String! email: String! avatarUrl: String posts(limit: Int): [Post!]! } type Post { id: ID! title: String! content: String! author: User! comments(limit: Int): [Comment!]! createdAt: String! } type Comment { id: ID! body: String! author: User! createdAt: String! }
! 表示非空约束,[] 表示列表类型。这个Schema就是前后端之间的契约:前端可以精确知道API支持哪些查询、每个查询需要什么参数、返回什么类型的数据。后端必须严格按照Schema实现,任何偏离都会在验证阶段被拦截。
有了这个Schema,开发工具可以做自动补全(你输入 user( 立刻提示 id: ID! 参数)、类型检查(查询里写了不存在的字段会报错)、文档生成(Schema本身就是文档)。这就是为什么GraphQL的开发体验往往比REST好——因为Schema把"文档"和"实现"统一了。
GraphQL的官方定义是:"A query language for your API, and a server-side runtime for executing those queries by using a type system you define for your data." 这句话包含三个不可分割的要素:
查询语言:客户端用来声明数据需求的语法。它长得像JSON的骨架——只有字段名没有值。查询的结构就是返回数据的结构,这是一种直观的对应关系。
类型系统:Schema定义了一切。所有查询必须在Schema的类型约束内,不存在"野生字段"。类型系统带来了可验证性、可自省性和可演化性。
服务器端运行环境:解析器(Resolver)负责为每个字段从实际数据源取数。数据源可以是数据库、微服务、第三方API、缓存——GraphQL引擎不关心数据从哪来,它只负责编排解析器的调用顺序,把结果组装成与查询同构的响应。
这三者缺一不可。没有查询语言,客户端无法精确声明需求;没有类型系统,查询就失去了约束和验证能力;没有运行环境,查询就只是一段文本,无法执行。
需要特别说明的是,GraphQL和REST不是非此即彼的关系。在工程实践中,它们各有适用场景:
GraphQL解决的是"复杂应用如何高效地获取和组合跨领域数据"这个具体问题,而不是要推翻REST的全部价值。理解这一点,比理解GraphQL的语法更重要。