跳转到内容
← 返回核心概念
软件工程实践计算机科学 · 软件工程19 分钟阅读

API 设计

API Design

2002 年前后,亚马逊的 CEO 杰夫·贝索斯发出了一道著名的内部指令(其内容直到 2011 年才被前亚马逊、时任 Google 工程师 Steve Yegge 在一篇广为流传的随笔中公开转述): 按 Yegge 的转述,备忘录末尾还附了一句威胁:"任何不遵从这一规定的人都会被开除。"这道"贝索斯备忘录"强制推行了一…

APIRESTGraphQL接口设计软件架构

2002 年前后,亚马逊的 CEO 杰夫·贝索斯发出了一道著名的内部指令(其内容直到 2011 年才被前亚马逊、时任 Google 工程师 Steve Yegge 在一篇广为流传的随笔中公开转述):

所有团队必须通过服务接口(API)暴露数据和功能。团队之间只能通过这些接口通信。不允许任何其他形式的进程间通信……没有例外。

按 Yegge 的转述,备忘录末尾还附了一句威胁:"任何不遵从这一规定的人都会被开除。"这道"贝索斯备忘录"强制推行了一个架构原则:每个功能必须以 API 的形式存在,能够独立部署和消费。十年后,这个实践的副产品就是 AWS(亚马逊云计算)——一个把亚马逊内部服务 API 化后对外销售的产品,成为全球最大的云计算平台。

破除误解:好 API 不是"功能齐全的 API"

初级工程师常认为,好的 API 应该暴露尽可能多的功能,让调用者有最大的灵活性。这个直觉是错的。

API 设计中最重要的原则之一来自 Joshua Bloch(Java 集合框架设计者):

API 的设计难点不在于增加功能,而在于知道哪些不应该加进来。每增加一个接口,就是一个你永远要维护的承诺。

API 一旦对外发布,改变它的成本极高——所有调用方需要同步更新。因此,最小化暴露(Minimal Surface Area)是 API 设计的黄金原则:只暴露必要的内容,延迟暴露那些不确定的内容。

REST:资源为中心的 Web API

REST(Representational State Transfer) 是 Roy Fielding 在 2000 年博士论文中提出的一组架构约束,描述了什么样的 Web 服务是"设计良好"的。REST 的核心约束:

  • 无状态(Stateless):每个请求携带所有必要信息,服务器不保存客户端会话状态
  • 统一接口(Uniform Interface):资源用 URL 标识,用 HTTP 方法(GET/POST/PUT/DELETE/PATCH)表达操作
  • 客户端-服务器分离:前后端可以独立演化
  • 可缓存(Cacheable):响应可以被缓存,提高性能

RESTful API 设计约定

GET    /users          → 获取用户列表
GET    /users/42       → 获取 ID 为 42 的用户
POST   /users          → 创建新用户(请求体包含数据)
PUT    /users/42       → 完整更新 ID 为 42 的用户
PATCH  /users/42       → 部分更新 ID 为 42 的用户
DELETE /users/42       → 删除 ID 为 42 的用户
```

响应使用 HTTP 状态码传达结果:

状态码含义
200 OK成功
201 Created资源已创建
400 Bad Request客户端请求格式错误
401 Unauthorized需要身份验证
403 Forbidden有身份但无权限
404 Not Found资源不存在
429 Too Many Requests限流
500 Internal Server Error服务端错误

版本控制:API 变化时,需要版本化以保持向后兼容。常见方式:URL 路径(/v1/users/v2/users)或请求头(Accept: application/vnd.myapi+json;version=2)。

破除误解:REST ≠ "用 JSON 的 HTTP 接口"

人们口中的"REST API",绝大多数并不符合 Fielding 的本意。2008 年,Fielding 专门写了一篇博文 REST APIs must be hypertext-driven,对"把任何基于 HTTP 的接口都叫 REST"表达了不满。他给出的判据很硬:

如果应用状态的引擎(也就是 API)不是由超文本(hypertext)驱动的,那它就不能算 RESTful,也不是 REST API。

换句话说,Fielding 眼中真正的 REST 还有一条常被忽略的约束——HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用状态引擎):响应里不只返回数据,还要返回"下一步能做什么"的链接,客户端靠这些链接在状态间导航,而不是把 URL 规则硬编码进代码。

Leonard Richardson 在 2008 年提出、经 Martin Fowler 推广的 Richardson 成熟度模型 把这条光谱分成四级:Level 0(只把 HTTP 当传输隧道,单一端点)、Level 1(引入资源 URL)、Level 2(正确使用 HTTP 方法与状态码)、Level 3(HATEOAS)。今天业界所谓的"RESTful",绝大多数停在 Level 2。

这不意味着 Level 2 是错的——它在工程上简单、好用、够好。但要明白:"REST"在日常语境里已经漂移成"资源风格的 HTTP+JSON 接口",和 Fielding 论文里的严格定义并不是一回事。知道这层区别,能让你在读规范、和较真的人讨论时不至于各说各话。

GraphQL:查询即接口

GraphQL 由 Facebook 在 2012 年内部开发,2015 年开源,提供了一种完全不同的 API 设计思路:

在 REST 中,服务器决定每个端点返回什么数据。客户端如果需要用户信息和他的帖子,可能需要发两个请求(/users/42/users/42/posts);或者单个请求返回了大量客户端不需要的字段(过度获取,Over-fetching)。

GraphQL 让客户端精确声明它需要什么:

graphql
query {
  user(id: 42) {
    name
    email
    posts(last: 5) {
      title
      createdAt
    }
  }
}
```

服务器只返回请求的字段,单次请求获取跨多个"资源"的数据。

GraphQL 的优势: - 消除过度获取和欠获取(Under-fetching) - 强类型 Schema,客户端可以利用 IDE 的自动补全和验证 - 单一端点(通常是 /graphql),减少接口数量

GraphQL 的挑战: - N+1 查询问题(为每个用户的每篇帖子单独查数据库)——需要用 DataLoader 批量化解决 - 缓存比 REST 复杂(URL 不再是缓存键) - 任意查询可能触发复杂计算,需要查询复杂度限制

Facebook、GitHub、Twitter(部分)、Shopify 使用 GraphQL。

gRPC:为服务间通信设计

RPC(Remote Procedure Call,远程过程调用)的思路比 REST 古老得多:让调用远程服务"看起来像调用本地函数"。这一范式的奠基之作是 Birrell 与 Nelson 1984 年的论文 Implementing Remote Procedure Calls,它被 1994 年 ACM 软件系统奖誉为"关于 RPC 的那篇论文"。RPC 长期有一个被反复诟病的陷阱:它假装网络是透明的,而网络其实会延迟、会丢包、会部分失败——这正是"分布式计算的谬误(Fallacies of Distributed Computing)"所警告的。好的 RPC 框架不掩盖这些现实,而是把超时、重试、流控显式暴露出来。

gRPC(Google Remote Procedure Call,2015 年 2 月开源;1.0 稳定版于 2016 年 8 月发布)正是现代 RPC 的代表:

面向服务间调用(微服务架构),而非浏览器-服务器通信。特点: - 使用 Protocol Buffers(二进制格式,比 JSON 更紧凑高效)定义接口 - 支持服务器流、客户端流、双向流 - 强类型 IDL(接口定义语言),自动生成多语言客户端 - 基于 HTTP/2,支持多路复用

protobuf
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
  rpc ListUsers (ListUsersRequest) returns (stream UserResponse);
}
```

gRPC 在微服务内部通信、IoT、移动后端等对性能要求高的场景被广泛采用。

API 设计的关键原则

向后兼容(Backward Compatibility):新版本 API 应该能处理为旧版本设计的客户端请求。实用原则:对新增字段保守(增加字段安全,删除字段危险);不改变已有字段的含义;新功能用新端点或可选参数。

幂等性(Idempotency):HTTP 的方法语义把两个常被混淆的概念分得很清(RFC 9110)。安全(safe) 指方法本质只读、不改变服务器状态——GET、HEAD、OPTIONS 属于此类。幂等(idempotent) 指重复调用与单次调用效果相同——GET、HEAD、PUT、DELETE 都是幂等的。两者的关系是:所有安全方法都幂等,但反过来不成立(PUT、DELETE 幂等却不安全,因为它们会改状态)。POST 和 PATCH 既不安全也不幂等。幂等性的工程价值在于:客户端在网络失败、不确定请求是否到达时,可以安全重试。对天然不幂等的 POST(如"创建一笔支付"),业界用幂等键(Idempotency Key)——客户端生成一个唯一键随请求发送,服务器对同一键只执行一次、后续重试直接返回首次结果(Stripe 的支付 API 即如此)。

分页(Pagination):列表接口不能一次返回全部数据,必须分页。两种主流策略各有取舍。基于偏移(offset/limit,如 ?page=3&size=20 直观、可跳页,但数据在翻页途中被插入或删除时会漏读或重读,且大 offset 在数据库里要扫过并丢弃前面所有行,越往后越慢。基于游标(cursor/keyset,如 ?after=<id> 用上一页最后一条记录的稳定排序键作为锚点,避免了上述问题,性能随页码恒定,代价是不能随机跳页。需要稳定、深翻的大数据集(信息流、导出)几乎都选游标分页。

限流(Rate Limiting):保护 API 不被单一客户端过度消耗。常用算法:令牌桶(Token Bucket)、漏桶(Leaky Bucket)、滑动窗口。响应头(X-RateLimit-LimitX-RateLimit-RemainingRetry-After)告知客户端限制状态。

错误信息的设计:错误响应应该告诉客户端为什么失败,以及如何修复(如果可能)。好的错误响应包含:HTTP 状态码、机器可读的错误代码(如 INVALID_EMAIL_FORMAT)、人类可读的错误消息、指向文档的链接(可选)。

API 文档(Specification):OpenAPI(原 Swagger)是 REST API 的事实标准文档格式,可以自动生成交互式文档(Swagger UI)和客户端 SDK。Swagger 由 Tony Tam 于 2011 年创建并开源;2015 年 11 月,规范在 Linux 基金会下成立的 OpenAPI Initiative(Google、IBM、微软等为创始成员)接手,2016 年初更名为 OpenAPI Specification,2017 年 7 月发布了大幅重构的 OpenAPI 3.0。今天"OpenAPI 是规范、Swagger 是工具集"已成共识。

代价与争议

REST vs GraphQL vs gRPC 的宗教战争:三种方式各有生态,没有绝对优劣。REST 最简单、最通用;GraphQL 在数据需求复杂的前端适合;gRPC 在内部服务间高性能调用适合。混合使用(对外 REST/GraphQL,内部 gRPC)是常见方案。

Hyrum 定律:你没承诺的,也会被依赖:API 是永久承诺,但承诺的范围其实比你写进文档的更大。Google 工程师 Hyrum Wright 提出了一条经验定律(经 Titus Winters 命名、收录于《Software Engineering at Google》而流传):

当一个 API 的使用者足够多时,你在契约里承诺了什么已经不重要了——系统所有可观察的行为,都会被某个人依赖上。

这意味着:返回字段的顺序、错误信息的具体措辞、未文档化的响应延迟,哪怕你从没保证过,只要有人观察到并依赖了,改动它就是一次破坏性变更。Hyrum 定律和前文 Bloch 的"最小化暴露"互为表里:Bloch 教你少暴露,Hyrum 提醒你暴露的远比你以为的多。这也是为什么严肃的 API 要尽早隐藏一切非必要细节——一旦泄漏,就再也收不回来了。

API 版本化的艰难:如何演进 API 而不破坏已有客户端,是 API 设计中最困难的长期工程问题。Stripe、Twilio 等公司维护了数十个并行版本的 API,这本身就是巨大的维护负担。Stripe 的做法颇具代表性:版本号以发布日期命名(如 2020-08-27),账号在首次调用时被自动"钉"在当时的最新版本上,此后所有请求隐式沿用该版本,除非显式通过 Stripe-Version 请求头覆盖或在后台升级。这样 Stripe 可以持续改动新版本对象结构,而老集成"活在它被钉住的那一天",完全不受影响——把"不破坏已有客户端"这一难题,转化成了可控的、用户自主节奏的迁移。

API 优先(API-First)设计:先设计 API(写 OpenAPI 规范),再实现。与先实现后文档化相对。API-First 使得前后端可以并行开发(前端基于规范 mock 数据),但需要在设计阶段做更多决策。

跨域连接

  • 分布式系统:客户端超时后无法区分"请求没到"与"到了但响应丢了",这条不确定性无法靠重试消除,只能靠语义化解——把操作做成幂等,或给它一把幂等键,让服务端对同一把键只执行一次。所以重试策略必须与接口语义一起设计,而不能留给调用方各自拿捏。
  • 环论:幂等不是约定而是代数性质:满足自我复合不变的操作,重复施加不改变状态,正如环里的幂等元。由此可判定哪些方法能无条件重试——完整替换与删除可以,创建一笔支付不行,除非外挂一把键把它变幂等。这也解释了幂等键为何该由客户端生成——只有它知道两次请求是不是同一个意图。
  • 科斯定理:企业边界由内部协调成本决定。那道禁止一切非接口通信的备忘录,等于强行把内部协调改成市场式交易,于是内部服务与可外销产品之间不再有技术差别——云业务正是这条推论的产物。推论是:一个组织内部的接口质量,长期看会决定它能不能把内部能力变成对外产品。
  • 语言如何变化:语言规范总是追认既成用法,而非相反。工程上的对应版本是:使用者足够多时,文档没承诺的行为也会被依赖,改动它就是破坏性变更。可检验推论:破坏成本随使用者规模超线性增长。
  • 法治:向后兼容对应法不溯及既往,版本化对应过渡条款。把账号钉在首次调用时的版本上,就是让存量主体继续适用旧规则,从而把"不许破坏"这条难以执行的禁令,换成由用户自主掌握节奏的迁移。代价是同时维护多份行为,版本越多,回归测试的组合就越大。

参考文献

  • Fielding, R. Architectural Styles and the Design of Network-based Software Architectures. Ph.D. dissertation, UC Irvine, 2000. (REST 的来源)
  • Fielding, R. REST APIs must be hypertext-driven. 2008. (https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven ,破除"HTTP+JSON 即 REST"的误解,HATEOAS 约束)
  • Fowler, M. Richardson Maturity Model. martinfowler.com, 2010. (REST 成熟度四级模型)
  • Fielding, R., Nottingham, M., & Reschke, J. (Eds.) RFC 9110: HTTP Semantics. IETF, 2022. (https://www.rfc-editor.org/rfc/rfc9110 ,safe / idempotent 方法语义的权威定义)
  • Birrell, A. D. & Nelson, B. J. Implementing Remote Procedure Calls. ACM Transactions on Computer Systems, 2(1), 1984. (RPC 范式的奠基论文)
  • Winters, T., Manshreck, T., & Wright, H. Software Engineering at Google. O'Reilly, 2020. (Hyrum 定律的出处与论述)
  • GraphQL Foundation. GraphQL Specification. spec.graphql.org.
  • Google. Introducing gRPC, a new open source HTTP/2 RPC Framework. Google Open Source Blog, 2015. (gRPC 开源公告,HTTP/2 + Protocol Buffers)
  • Stripe. APIs as infrastructure: future-proofing Stripe with versioning. stripe.com/blog/api-versioning. (日期版本号 + 账号钉版机制)

延伸阅读

  • Bloch, J. How to Design a Good API and Why It Matters. Google Tech Talks, 2007. (https://www.youtube.com/watch?v=aAb7hSCtvGw)
  • Kleppmann, M. Designing Data-Intensive Applications. O'Reilly, 2017. (第 4 章:编码和演化,讨论 API 向后兼容)
  • Richardson, L. & Amundsen, M. RESTful Web APIs. O'Reilly, 2013.