API 设计的哲学:为什么 REST 和 GraphQL 都不是答案

> "工具本身从来不是问题的答案,理解问题才是。" —— 未署名的架构师

引言:一场永无止境的争论

在技术社区里,关于 API 设计的争论从未停止。REST 的支持者会告诉你:"遵循 RESTful 原则,你的 API 就会优雅、可扩展。" GraphQL 的拥趸则反驳:"单一端点、精确查询、类型安全,这才是现代 API 的未来。"

但如果我告诉你,这场争论本身就是一个错误的问题?

真正的问题不是"应该选择 REST 还是 GraphQL",而是我们为什么需要在两种范式之间做非此即彼的选择?更深层次的问题是:我们是否真正理解了 API 设计背后的本质问题?

REST 的谎言:资源导向的美丽陷阱

REST(Representational State Transfer)在 2000 年由 Roy Fielding 提出时,是一个革命性的概念。它将 Web 服务抽象为"资源"的集合,通过标准的 HTTP 方法(GET、POST、PUT、DELETE)进行操作。听起来很完美,对吧?

但现实是:大多数声称自己是 RESTful 的 API,其实只是 HTTP-based API

问题一:真实世界不是资源的简单集合

想象一下这个场景:你需要设计一个 API 来处理"用户下单购买商品"的业务逻辑。按照 REST 的思路,这是一个什么操作?

  •  
    1. 创建一个订单资源?(POST /orders)
    2. 更新商品库存?(PUT /products/{id})
    3. 更新用户的购买历史?(PUT /users/{id}/purchase-history)

你很快会发现,这个"简单"的业务操作,在 REST 的世界里被拆分成了多个资源操作。更糟糕的是,这些操作之间有复杂的依赖关系和事务性要求。如果其中一个失败了,你该如何回滚?

真实的业务逻辑往往是行为驱动的,而不是资源驱动的。强行将其套入资源模型,就像试图用螺丝刀拧钉子——技术上可行,但并不合适。

问题二:过度获取与不足获取的困境

REST 的另一个经典问题是:你无法精确地告诉服务器你需要什么

假设你有一个 /users/{id} 端点,返回用户信息。对于移动端列表页,你只需要用户名和头像;但对于详情页,你需要用户的完整信息、发布的文章、关注者列表等。你会怎么做?

  •  
    1. 方案 A:创建多个端点(/users/{id}/brief/users/{id}/full)
    2. 方案 B:使用查询参数(/users/{id}?fields=name,avatar)
    3. 方案 C:让客户端调用多个端点拼装数据

每一种方案都有明显的缺陷。方案 A 导致端点爆炸;方案 B 让 API 变得复杂且难以维护;方案 C 则带来性能问题和网络开销。

这不是实现问题,而是 REST 范式本身的局限性。

问题三:版本管理的噩梦

当你的 API 需要演进时,REST 的版本管理就成了一个棘手的问题。常见的做法包括:

  •  
    1. URL 版本:/v1/users/v2/users
    2. Header 版本:Accept: application/vnd.myapi.v2+json
    3. 参数版本:/users?version=2

但无论哪种方式,你都面临一个根本性问题:版本边界在哪里?是整个 API 一起升版本,还是每个端点独立升版本?如果是后者,版本组合的复杂度会呈指数增长。

GraphQL 的幻觉:灵活性的代价

GraphQL 的出现,确实解决了 REST 的一些痛点。单一端点、精确查询、强类型系统——这些特性让它在 2015 年开源后迅速走红。

但 GraphQL 带来了新的、更隐蔽的问题。

问题一:查询复杂度的黑洞

GraphQL 的核心卖点是"客户端可以精确指定需要什么数据"。但这个优势的反面是:客户端可以构造任意复杂的查询

考虑这个看似无害的查询:

query {
users {
posts {
comments {
author {
posts {
comments {
author {
# 无限嵌套...
}
}
}
}
}
}
}
}

这是一个合法的 GraphQL 查询,但它可能导致:

  •  
    1. 数据库的N+1 查询问题爆炸
    2. 服务器 CPU 和内存资源耗尽
    3. 响应时间从毫秒级跳到分钟级

你需要实现查询深度限制、复杂度计算、速率限制等机制。但这些机制本身又引入了新的复杂度:如何定义"合理"的查询复杂度?

 

问题二:缓存的噩梦

REST 的一个巨大优势是:它天然契合 HTTP 缓存机制。每个 URL 代表一个资源,可以用 ETag、Cache-Control 等标准 HTTP 头进行缓存。

GraphQL 呢?所有查询都发往同一个 POST 端点。传统的 HTTP 缓存机制完全失效。你需要:

  •  
    1. 实现客户端缓存(如 Apollo Client 的标准化缓存)
    2. 在服务器端维护查询缓存
    3. 处理缓存失效的复杂逻辑

你用灵活性交换了简单性。

 

问题三:权限控制的碎片化

在 REST 中,权限控制相对直观:每个端点可以有自己的权限规则。但在 GraphQL 中,权限控制必须下沉到字段级别

这意味着:

  •  
    1. 权限逻辑散布在 resolver 函数中
    2. 难以全局审查和管理权限规则
    3. 容易出现权限漏洞(某个 resolver 忘记检查权限)

更糟糕的是,客户端可能请求了 100 个字段,但其中 3 个字段没有权限。你该如何处理?返回部分数据?抛出错误?这些决策都没有标准答案。

 

真正的问题:我们在解决什么?

REST 和 GraphQL 的支持者都犯了同一个错误:他们把技术范式当成了答案,而不是工具

真正重要的问题是:

  1. 你的 API 的主要使用场景是什么?

- 公共 API vs. 内部服务?
- 移动端 vs. Web 端 vs. 服务间调用?
- 高频简单查询 vs. 低频复杂查询?

 

  1. 你的团队的技术栈和能力如何?

- 团队对 REST/GraphQL 的熟悉程度?
- 现有的基础设施支持?
- 运维和监控能力?

  1. 你的性能和可扩展性要求是什么?

- QPS 目标是多少?
- 可接受的延迟范围?
- 数据规模有多大?

  1. 你的 API 的生命周期是怎样的?

- 预期会频繁演进,还是相对稳定?
- 有多少客户端,它们的升级能力如何?

第三条路:混合与务实

在实际的生产环境中,最成功的 API 策略往往不是纯 REST 或纯 GraphQL,而是混合式、务实式的设计。

案例一:GitHub API

GitHub 同时提供 REST API 和 GraphQL API:

  •  
    1. REST API:用于简单的 CRUD 操作,如获取仓库信息、创建 issue
    2. GraphQL API:用于复杂的查询场景,如获取用户的贡献图谱

这不是犹豫不决,而是为不同场景选择合适的工具

 

案例二:Netflix

Netflix 的 API 架构更为激进:他们使用了一个名为 Falcor 的内部框架,它既不是 REST 也不是 GraphQL,而是基于JSON Graph的概念:

  •  
    1. 客户端可以用类似 GraphQL 的语法查询数据
    2. 但底层是高度优化的缓存和批处理机制
    3. 针对视频流媒体的特定场景深度定制

没有完美的通用解决方案,只有适合特定问题的最优解。

 

案例三:Shopify

Shopify 的演进路径很有启发性:

  •  
    1. 早期:纯 REST API
    2. 2016 年:引入 GraphQL API
    3. 现在:REST API 用于简单操作和第三方集成,GraphQL 用于 Shopify Admin 等复杂界面

关键点:他们没有废弃 REST,而是让两者共存,各取所长。

 

设计原则:超越范式的思考

如果不拘泥于 REST 或 GraphQL,我们应该遵循什么样的 API 设计原则?

原则一:以用例为中心,而非以技术为中心

不要先选择技术,再把业务逻辑塞进去。而是:

  1. 明确核心用例(前 5 个最重要的 API 调用场景是什么?)
  2. 为这些用例设计最优的接口
  3. 再考虑用什么技术实现

例如,如果你的核心用例是"获取用户 feed 流",那么:

  •  
    1. 单独设计一个 /feed 端点可能比试图用 REST 的 /posts?user=...&filter=... 更合适
    2. 或者用 GraphQL 的 query { feed { ... } } 更灵活

关键是用例驱动,而非范式驱动。

 

原则二:拥抱约束,而非追求完美的灵活性

GraphQL 的一个诱惑是"无限灵活性"。但在实际中,约束往往比灵活性更有价值:

  •  
    1. 明确的端点数量(REST)让 API 更容易理解和监控
    2. 预定义的查询模板(GraphQL Persisted Queries)提高安全性和性能
    3. 渐进式披露(Progressive Disclosure):先提供简单接口,再暴露高级功能

 

原则三:优化常见路径,容忍边缘情况

 

帕累托原则(80/20 法则)在 API 设计中同样适用:

  •  
    1. 识别出占 80% 流量的 20% 核心接口
    2. 为这些核心接口投入 80% 的优化精力
    3. 边缘情况可以用"不够优雅"但有效的方式处理

例如:

  •  
    1. 核心列表查询用优化过的 REST 端点
    2. 复杂的管理后台查询用 GraphQL
    3. 批量操作用专门的批处理端点

 

原则四:可演进性高于初始完美性

 

API 的最大成本不是初始开发,而是长期演进和维护。好的 API 设计应该:

  •  
    1. 预留扩展点(extension points)
    2. 使用语义化的版本策略
    3. 提供清晰的废弃和迁移路径
    4. 文档和实现同步演进

这意味着:

  •  
    1. 不要过度设计初始版本
    2. 但要为未来变化留下空间
    3. 用真实的使用反馈驱动演进

 

结论:成为工具的主人,而非奴隶

 

REST 和 GraphQL 都不是答案,因为它们不是答案,而是工具。真正的答案是:

  1. 深刻理解你要解决的问题
  2. 根据具体场景选择或组合工具
  3. 持续迭代和优化

最糟糕的决策不是选择了 REST 或 GraphQL,而是:

  •  
    1. 盲目跟风,因为"大厂都在用 GraphQL"
    2. 教条主义,坚持"必须 100% RESTful"
    3. 拒绝演进,认为"一旦选定就不能改"

好的架构师不是坚持某种范式,而是知道何时打破规则。

 

下次当团队再次陷入"REST vs. GraphQL"的争论时,不妨停下来问自己:

  •  
    1. 我们真正要解决的问题是什么?
    2. 这个问题的核心约束是什么?
    3. 什么样的接口设计最适合我们的用户和团队?

也许你会发现,答案既不是纯 REST,也不是纯 GraphQL,而是一个为你的特定场景量身定制的混合方案

 

而这,才是真正的 API 设计艺术。


思考题:

  1. 回顾你参与过的项目,有多少 API 设计决策是基于"大家都这么做"而非实际需求分析?
  2. 如果让你重新设计当前项目的核心 API,在不受现有技术栈限制的情况下,你会如何设计?
  3. 你的团队是否有明确的 API 设计原则文档?如果没有,是否应该建立一个?

延伸阅读:

  •  
    1. Roy Fielding 的博士论文:"Architectural Styles and the Design of Network-based Software Architectures"
    2. GraphQL 官方文档的 Best Practices 章节
    3. Martin Fowler 的 "Richardson Maturity Model" - 理解 REST 的成熟度层次

 

You voted 2. Total votes: 24

添加新评论