RESTful API 设计最佳实践:资源建模、状态码与版本化

REST(Representational State Transfer)是目前 Web API 最主流的架构风格。微软在 Azure 架构中心发布的《Web API 设计最佳实践》与 Google 的 AIP(API Improvement Proposals)设计规范,共同构成了业界公认的设计依据。无论你用的是 Node.js ExpressPython FastAPI 还是 Go Gin,遵循这套约定都能让接口更易理解、更易维护。

一、资源建模:用名词而不是动词

RESTful API 围绕"资源"组织,每个资源由唯一的 URI 标识。设计 URI 时要记住三条核心约定:

  1. 用复数名词表示集合/orders 是订单集合,/orders/5 是单个订单。
  2. 不要在 URI 里用动词:HTTP 方法本身已经表达了动作,POST /orders 是创建订单,而不是 /create-order
  3. 关系保持简单:推荐 collection/item/collection 这种层级,例如 /customers/1/orders,避免过深的嵌套如 /customers/1/orders/99/products

此外,微软明确建议不要让 API 直接镜像数据库表结构。REST 建模的是业务实体和操作,而不是把每张表都暴露成资源——这既增加了攻击面,也可能造成数据泄露。必要时在数据库与 API 之间加一层映射。

二、HTTP 方法与状态码:语义要对齐

每种 HTTP 方法都有明确语义,状态码要能准确反映结果:

方法 语义 常见状态码
GET 读取资源 200、204、404
POST 创建资源 201(Location 头带新资源 URI)、400、405
PUT 整体更新,必须幂等 200、201、204、409
PATCH 部分更新 200、400、409、415
DELETE 删除资源 204、404

PATCH 推荐使用 JSON Patch(RFC 6902)或 JSON Merge Patch(RFC 7396);PUT 必须是幂等的,重复提交结果一致,而 POST 和 PATCH 不保证幂等。对耗时操作(如导出报表),可以返回 202 Accepted 并提供状态查询端点,客户端通过轮询获取进度。

三、分页、过滤与排序

大数据集不要一次全量返回。使用查询参数:

GET /orders?limit=25&offset=0
GET /orders?status=shipped&sort=price
  • limit / offset:控制返回条数与起始位置,并设置上限防止 DoS。
  • 过滤:通过查询字符串传条件,如 ?status=shipped
  • 排序:用 sort=price 指定排序字段。
  • 字段选择:用 fields=id,name 让客户端只取需要的字段。

注意:排序会影响缓存命中率,因为查询字符串参与缓存键。

一个规范接口的完整例子

把前面的规则拼起来,一个规范的订单查询接口大概长这样:

GET /v1/orders?status=paid&limit=10&offset=0
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{
  "items": [
    { "id": "ord_8f2a", "customer_id": "cus_31", "total": 1250, "status": "paid" }
  ],
  "pagination": { "limit": 10, "offset": 0, "total": 42 }
}

注意几个细节:资源用复数名词 orders;版本放在 URI 前缀 /v1;分页参数显式出现在查询串里;响应统一包一层 pagination,而不是让客户端自己去猜总条数。

错误响应的统一格式

错误处理是最容易各写各的部分。建议全站统一一种错误体,客户端只需解析一次:

{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "Order ord_9x1b was not found",
    "detail": { "resource": "orders", "id": "ord_9x1b" }
  }
}

code 用机器可读的枚举,方便客户端做分支判断;message 面向人类;detail 放可选的上下文信息。配合 404、409、422 等状态码,前端和监控都能快速定位问题。

幂等性的实际考量

PUT 要求幂等,但"整体更新"并不总能覆盖所有业务需求。更稳妥的做法是给写接口加一个 Idempotency-Key 请求头:客户端为每次"逻辑上只应执行一次"的操作生成一个随机键,服务端按键去重。支付、下单这类接口尤其值得加上——网络超时后客户端重试,也不会重复扣款或重复下单。

四、版本化:兼容旧客户端

API 不会一成不变。微软列出了四种版本化方案:

  • URI 版本化/v2/customers/3,简单直观,但版本多了难维护。
  • 查询字符串版本化/customers/3?version=2,同一资源同一 URI。
  • Header 版本化:自定义头如 api-version: 2,不污染 URI。
  • 媒体类型版本化Accept: application/vnd.contoso.v2+json,最 RESTful 也最复杂。

对大多数中小型项目,URI 版本化最简单、对缓存最友好,是常见首选。

版本化不是发布当天的动作,而是一套提前规划的契约。一个常见场景:你的 API 已经有第三方客户端在调用,却要把 /customers/3 的响应从扁平结构改成嵌套结构。如果直接改,老客户端会立刻解析失败。更稳妥的做法是发布 /v2 新版本,保留 /v1 运行一个过渡期(例如 6 个月),在文档里标注弃用时间,并通过 Deprecation 响应头告知客户端;等旧版本调用量明显下降后再下线 /v1。这个节奏比"一次大改、全员迁移"稳得多,也更容易争取到客户的配合窗口。

五、OpenAPI:契约优先

微软推荐采用 OpenAPI(OAS)做契约优先设计:先定义接口契约,再实现代码。Swagger/OpenAPI 工具可以从契约自动生成文档和客户端库。Google 的 AIP 还提供了 API Linter(linter.aip.dev)来自动检查设计规范。

如果对 REST 和 GraphQL 的取舍还不确定,可以先读 REST vs GraphQL 对比与选型指南,再结合 网站 API 集成基础指南 理解调用方视角。

参考:微软官方《Web API 设计最佳实践》 https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
参考:Google AIP 设计规范 https://google.aip.dev/

16IDC 观察

对独立开发者和小团队,API 设计规范是"一次投入、长期受益"的事:接口命名统一后,前端对接、自动化测试和后续迭代都会顺畅很多。上线前记得规划好 API 错误处理与重试策略,并把接口文档纳入 CI。更多后端工程实践见 后端对接 分类。

原文来源:https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design