打个比方,咱们在聊 API 设计的时候,其实就是在聊“客户端怎么跟服务器要数据”。这事儿说复杂也复杂,说简单也简单。目前江湖上两大门派,一个是老大哥 REST,一个是新贵 GraphQL。
先聊聊 REST。这玩意儿大家肯定都熟,全名叫 Representational State Transfer。虽然听着挺学术,但核心思想特别接地气:资源导向。你把一个数据对象(比如用户、订单)看作一个资源,给它一个 URI 地址,然后用 HTTP 自带的方法(GET、POST、PUT、DELETE)去操作它。REST 最大的优势在于它是“HTTP 原教旨主义者”,特别依赖 HTTP 协议本身的东西,比如状态码(404、500)、头部信息,还有最牛逼的缓存机制(ETag、Cache-Control)。
根据最新的 OpenAPI 3.1.0(2022年2月发布的,现在都2024了,这版本已经非常成熟了,还兼容 JSON Schema 2020-12),REST 的设计越来越规范。REST 讲究无状态,服务器不记着你上次干了啥,每次请求都得带齐所有信息,这样扩展起来贼方便,随便加机器。
再看看 GraphQL。这货是 Facebook 搞出来的,现在规范版本是 June 2018 (RFC 8257),虽然规范定得早,但生态一直在更新,像 graphql-js 这种核心库现在都维护到 16.x-17.x 版本了。GraphQL 跟 REST 最大的不同是:它只有一个端点。不管你要啥数据,统统往 /graphql 这个地址发 POST 请求。
GraphQL 最爽的地方在于它的声明式数据获取。咱们以前用 REST 经常遇到“过度获取”(返回了一堆用不上的字段)或者“获取不足”(为了拿个关联数据,还得再发一次请求,也就是经典的 Waterfall 请求)。GraphQL 让你在查询里精确指定你要哪些字段,服务器就只返回哪些字段。而且它有个强类型的 Schema,这玩意儿就像一份合同,前后端都按这个来,还能内省(Introspection),直接查服务器支持啥接口,这简直是写文档和生成类型的神器。
⚡ 效率提示:如果你在做移动端开发,或者你的前端页面数据结构特别复杂(比如那种嵌套了七八层的 Dashboard),别犹豫,上 GraphQL。它能帮你省下不少往返流量,也能减少前端处理数据拼接的痛苦。但如果你只是做个简单的 CRUD,或者对外提供公开的、不需要太灵活查询的接口(比如支付接口),REST 依然是首选,毕竟生态成熟,调试工具满天飞。
假设我们要获取一个用户的详细信息,包括他发的帖子。
REST 风格:
你可能需要先请求 /users/1,拿到用户数据后,再请求 /users/1/posts。这就是典型的“获取不足”。
GraphQL 风格:
一次请求搞定,而且只要 name 和 posts 里的 title。
光说不练假把式,咱们直接上手写代码。这一节咱们不聊虚的,直接看怎么定义接口。
先说 REST 这边。现在业界标准基本就是 OpenAPI 规范(以前叫 Swagger)。最新的是 3.1.0 版本,这个版本最大的亮点就是完全兼容 JSON Schema 2020-12,这意味着你在定义数据结构时,能用到更现代的 JSON Schema 特性。写 OpenAPI 文档,写一个 YAML 或者 JSON 文件,把你的接口、参数、返回值都描述清楚。这玩意儿不仅能当文档看,还能直接用代码生成工具生成前端 TypeScript 类型或者后端接口代码。
再看 GraphQL。GraphQL 不需要什么 YAML 文件,它的核心就是 Schema Definition Language (SDL)。你直接在代码里定义类型(Type)、查询(Query)和变更(Mutation)。GraphQL 的 Schema 是强类型的,这就保证了你传错参数或者返回错类型,在开发阶段甚至编译阶段就能报错,不用等到上线才抓瞎。
很多新手容易把 GraphQL 当成数据库查询语言,这是大错特错的。GraphQL 只是一个API 查询语言,它不管你数据存在哪儿,是 MySQL、MongoDB 还是第三方 API,它只负责把你查询的字段映射到后端的 Resolver 函数上。
REST (OpenAPI 3.1.0) 定义:
咱们用 YAML 格式,定义一个获取文章列表的接口。
GraphQL (SDL) 定义:
咱们用 GraphQL 的语法定义同样的文章类型,并写一个查询入口。
后端 Resolver 示例 (Node.js + graphql-js 风格):
⚡ 效率提示:如果你团队里前端特别多,或者需要对接很多第三方,REST + OpenAPI 的可读性和工具链(比如 Swagger UI)对新手非常友好。但如果你团队是全栈居多,且追求极致的开发体验(比如类型安全一路到底),GraphQL 的 Schema 绝对能让你爽到飞起。不过记得,GraphQL 的 Schema 一旦发布,修改字段要考虑兼容性,最好用 @deprecated 指令而不是直接删。
到了进阶阶段,咱们就得聊聊那些“坑”了。这两个技术都有各自的软肋,踩过的人才知道疼。
先说 GraphQL 的 N+1 问题。这简直是 GraphQL 初学者的噩梦。啥意思呢?假设你查了 10 篇文章,每篇文章都要去查一下作者是谁。如果你在 Resolver 里直接写查询逻辑,可能会导致你发了 1 个请求查文章,然后又发了 10 个请求去查作者(1 + N)。这在 REST 里很少见,因为你通常会写个 JOIN 语句一次性搞定。但在 GraphQL 的 Resolver 链式调用里,这太容易发生了。
解决办法也很成熟,就是使用 DataLoader。这是 Facebook 官方出的一个工具库,核心思想是批处理(Batching)和缓存(Caching)。它会在同一个事件循环里,把一堆请求 ID 收集起来,然后一次性发给数据库,再把结果分发给对应的 Resolver。
再说 REST 的缓存。REST 的缓存是它最大的护城河。因为 REST 是基于 URL 的,浏览器和 CDN 天然就能缓存 GET /users/1 这个地址。你只要加个 Cache-Control 头,或者搞个 ETag,剩下的事儿浏览器都帮你干了。但 GraphQL 就不一样了,它只有一个 POST 端点 /graphql,传统的 HTTP 缓存基本废了。虽然现在有一些方案,比如把查询参数放到 URL 里(GET 请求)或者用 persisted queries,但复杂度明显上去了。
GraphQL 的 N+1 问题不是不能解决,而是你必须得在写代码的时候有这个意识。就像写 SQL 要防注入一样,写 GraphQL Resolver 要防 N+1。
假设我们有个场景,查文章列表,同时需要解析每篇文章的作者。
没有 DataLoader 的“坑爹”写法(千万别这么干):
使用 DataLoader 的正确姿势:
REST 这边,咱们不需要啥花里胡哨的库,直接用 HTTP 头搞定。
当客户端下次请求时,如果支持缓存,会带上这个:
如果服务器发现资源没变,直接返回 304 Not Modified,连 body 都不用传,省流量。
💡 经验总结:在 GraphQL 项目中,别以为用了 DataLoader 就万事大吉了。DataLoader 的缓存是基于单次请求生命周期的(Request Scoped)。如果你需要跨请求的缓存(比如 Redis),还得自己在 DataLoader 的批处理函数里加一层缓存逻辑。对于 REST,如果你的 API 是给移动端用的,记得在客户端也做好缓存策略,别啥都往服务器请求,那样 CDN 再牛也救不了你的服务器带宽。
换个角度看,选 GraphQL 还是 REST,不能光看谁火,得看你具体的业务场景。作为一个踩过不少坑的工程师,我见过太多团队为了跟风盲目上 GraphQL,结果把简单的项目搞复杂了,也见过死守 REST 导致前端吐槽接口难用到想砸键盘的。
咱们先聊聊 BFF(Backend for Frontend)模式。这是 GraphQL 的主战场。想象一下,你在做一个复杂的 Dashboard 页面,既要显示用户信息,又要显示他最近的订单,还要显示推荐商品。如果你用 REST,大概率得发三个请求:/user, /orders, /recommendations。这就是典型的“请求瀑布”。用 GraphQL 就不一样了,一个请求搞定,而且你要什么字段,它返回什么字段,绝不多给。
特别是在移动端,带宽和电量都是金贵的。GraphQL 的 强类型模式 (Schema) 这时候就很有用了。配合像 graphql-js (目前主流版本在 16.x-17.x 系列,2023-2024 还在持续维护) 这样的库,你可以很清晰地定义数据结构。
来看个典型的 BFF 场景下的 GraphQL 查询示例,假设我们要从多个微服务聚合数据:
对应的 Node.js (Apollo Server) Resolver 代码大概长这样,这里有个坑要注意,如果不处理 N+1 问题,查订单详情时数据库可能会被打爆:
⚡ 效率提示:如果你的后端是那种简单的 CRUD,或者你正在做一个对外的、需要被第三方广泛集成的公共 API(比如像 Stripe 那样的支付接口),别犹豫,直接用 REST。REST 的 无状态性 和 统一接口 让它非常容易理解和调试。而且,REST 可以充分利用 HTTP 协议自带的缓存,比如 ETag 和 Cache-Control,这对公开的 API 来说简直是开箱即用的福利。
但是,如果你是在做微服务架构的内部通信,尤其是前端需要聚合多个服务的数据时,GraphQL 配合 Apollo Federation 简直是神器。简单来说,REST 在微服务下容易导致“网关爆炸”,你得在网关层写一堆聚合代码,而 GraphQL 的联邦架构可以让每个微服务自己管理自己的 Schema 片段,最后自动组合。
再说说高并发。REST 因为可以用 GET 请求配合 CDN 缓存,应对高并发读取简直是手到擒来。而 GraphQL 通常只暴露一个 /graphql 端点,且多数是 POST 请求,缓存起来比 REST 麻烦不少。虽然现在有 APQ (Automatic Persisted Queries) 之类的技术,但复杂度确实上去了。所以,读多写少、对缓存要求极高、结构简单的场景,REST 依然是王者。
2024 年了,API 的玩法又变了。作为一个紧跟技术趋势的博主,我发现现在的风向标已经不仅仅是“谁快谁慢”,而是“谁更能融入现代前端生态”和“谁更能支撑复杂的企业级架构”。
首先必须提 React Server Components (RSC)。这玩意儿出来后,GraphQL 的处境其实有点微妙但也更有趣了。以前我们用 GraphQL 是为了减少水合(Hydration)的数据量,现在 RSC 允许组件在服务端直接渲染,那 GraphQL 的角色就变成了服务端与数据源之间的桥梁。现在流行的一种模式是:RSC 组件内部直接发起 GraphQL 请求获取数据,然后流式传输给客户端。配合 GraphQL 的 增量交付 (Defer/Stream) 特性,你可以先把页面骨架渲染出来,然后慢慢把重的数据“流”过来。
看个在 Next.js 14+ (支持 RSC) 环境下使用 GraphQL 的例子,这比传统的 useEffect 那种客户端请求优雅多了:
再聊聊企业级架构。Apollo Federation 现在基本上成了微服务下 GraphQL 的“标准答案”。换个角度看,它让你不用维护一个巨大的单体 GraphQL Schema,而是把 Schema 拆散到各个微服务里。比如用户服务管 User 类型,订单服务管 Order 类型,它们通过联邦指令自动拼成一个完整的图。
💡 经验总结:如果你公司的微服务数量超过 5 个,且前端需要一个统一的入口,一定要调研下 Apollo Federation。别自己手写聚合代码,那是 2020 年的老黄历了。
最后看看 REST 这边。虽然 GraphQL 风头正劲,但 REST 也没闲着。OpenAPI 3.1.0 (2022年2月发布) 已经兼容 JSON Schema 2020-12 了,这意味着它在定义复杂数据结构时比以前强太多。而且,随着 HTTP/3 的普及,REST 的性能瓶颈进一步被打破。HTTP/3 基于 QUIC 协议,解决了队头阻塞问题,这意味着即使你用 REST 发一堆小请求,速度也不会像以前 HTTP/1.1 那样卡顿。
另外,社区里 OpenAPI 与 AsyncAPI 的融合 也是个大趋势。以前 REST 是同步的,想搞实时或者事件驱动还得靠 WebSocket 自己定义协议。现在 AsyncAPI 让你能用类似 OpenAPI 的方式定义事件,这对做物联网或者实时通知系统来说,简直是福音。
对于面试或者技术选型来说,记得关注 REST 的幂等性。比如 PUT 和 DELETE 是幂等的,而 POST 不是。这在设计高并发下的重试机制时至关重要。而 GraphQL 面试必问的就是 Resolver 的性能优化,特别是 N+1 问题,不把 DataLoader 讲清楚,面试官基本不会让你过。
换个角度看,2024 年不是谁取代谁的问题,而是 GraphQL 在复杂交互和 BFF 领域深耕,REST 在公开 API 和高性能传输领域继续称王。甚至很多大厂是“混着用”:内部微服务用 GraphQL 联邦,对外暴露给合作伙伴的用 REST 和 OpenAPI。这种“双模 API”架构,才是目前最务实的选择。