RESTful API 设计原则:资源、动词与状态码

FreeGuideOnline 最新 2026-06-13

理解 REST:万维网的架构基石

REST(Representational State Transfer,表述性状态转移)并非一种协议或标准,而是一组用于设计网络应用的架构约束与原则。遵循这些原则构建的 API,我们称之为 RESTful API。它的核心理念是将服务端的一切都建模为资源,并通过标准的 HTTP 方法(动词)对其进行操作,同时利用 HTTP 状态码传达清晰的操作结果。

核心设计优势

  • 无状态通信:每个请求都包含理解该请求所需的全部信息,服务器无需保存客户端会话上下文,极大提升了系统的可伸缩性。
  • 统一接口:通过一套标准化的约定(资源标识、动词、媒体类型等)解耦客户端与服务端,使得双方可以独立演化。
  • 可缓存性:显式的缓存控制减少了客户端与服务器间的交互,提升了性能和用户体验。
  • 分层系统:客户端无法也无须知道自己是直接连接到了终端服务器,还是中间代理,这为负载均衡、安全策略的实施提供了便利。

原则一:将一切建模为资源

在 RESTful 的世界里,你交付的不是“执行某个动作”,而是“某个资源在特定时刻的表述”。资源可以是一个文档、一张图片、一个用户,甚至是一项服务的集合。

资源命名之道

资源的 URI(Uniform Resource Identifier)应只用于标识资源,而非描述对资源的操作。一个优秀的 URI 设计应当是名词化的、层级清晰的。

  • 使用名词复数形式/users 表示用户集合,/users/1024 表示单个用户。
  • 利用路径表示层级关系/users/1024/orders 表示属于该用户的所有订单,/users/1024/orders/5 表示该用户的第5号订单。
  • 避免动词:不要设计成 /getUser/createOrder,操作由 HTTP 动词承载。
  • 过滤、排序和分页应作为查询参数/users?role=admin&sort=name&page=2 本质上是对同一资源(用户集合)的不同视图。

反例与正例对比

❌ POST /createUser
❌ GET /getUserOrders?userId=1024

✅ POST   /users          (创建用户)
✅ GET    /users/1024/orders  (获取用户1024的订单)

资源粒度与子资源

避免设计过浅或过深的嵌套。通常建议资源嵌套不超过三层。如果业务确实复杂,可以考虑通过根资源配合查询参数来扁平化结构。

# 可以接受
/users/1024/orders/5/items

# 太深,可改为通过根资源操作
/items?orderId=5&userId=1024

原则二:用标准 HTTP 动词定义操作

REST 利用 HTTP 协议本身的方法(动词)来表明对资源执行的动作。每个动词都应具备安全幂等的属性,客户端和服务端据此可以放心地进行重试。

动词 操作语义 安全性 幂等性 示例请求 URI 请求体内容
GET 获取资源表述 /users/1024
POST 创建子资源 /users 新用户的 JSON 数据
PUT 完整替换资源 /users/1024 更新后的完整用户 JSON 数据
PATCH 部分更新资源 /users/1024 仅包含更改字段的 JSON 数据
DELETE 删除资源 /users/1024
HEAD 获取元信息 /users/1024
OPTIONS 获取允许操作 /users

安全性:调用不会对服务器状态产生任何副作用,可任意缓存。
幂等性:操作执行一次与执行多次的效果相同。对于 PUT,无论请求发送多少次,资源最终状态都等同于第一次请求后的状态(前提是请求体相同)。POST 不具幂等性,重复发送会创建多个资源。

动词选择实战

创建资源:POST vs PUT

当客户端不具备生成资源标识符的能力(即让服务器自动生成ID)时,使用 POST 向集合资源发送请求。

POST /users
Content-Type: application/json

{ "name": "Alice", "email": "alice@example.com" }

若客户端能够指定资源的唯一标识(例如使用 UUID),则可用 PUT 创建资源。

PUT /users/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{ "name": "Alice", "email": "alice@example.com" }

更新资源:PUT vs PATCH

PUT 要求提供资源的完整表述,缺失的字段将被置空或覆盖为默认值,因此使用前最好先 GET 获取现有数据再合并提交。PATCH 仅需发送需要更改的字段,适合部分更新场景。

# 完整替换(注意:未传递的字段可能会被清掉)
PUT /users/1024
{ "name": "Alice", "email": "new@example.com", "age": 30 }

# 仅更新邮箱
PATCH /users/1024
{ "email": "new@example.com" }

批量操作

传统 REST 不直接定义批量操作,通常折衷方案为设计一个单独的批量资源:

POST /users/batch
{ "action": "delete", "ids": [1, 3, 5] }

或使用 207 Multi-Status 响应返回每个子操作的结果。但在设计初期,优先思考能否将业务拆解为多个单一请求,以保持接口的纯粹性。

原则三:用 HTTP 状态码传达明确语义

响应状态码让客户端无需解析响应体即可快速判断请求结果。使用标准的 HTTP 状态码家族,比自定义业务码更通用、更易维护。

状态码分类速查

状态码范围 含义 常用示例
2xx 请求成功 200 OK, 201 Created, 204 No Content
3xx 重定向 301 Moved Permanently, 304 Not Modified
4xx 客户端错误 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity
5xx 服务器端错误 500 Internal Server Error, 503 Service Unavailable

常用状态码场景指南

  • 200 OK:GET、PUT、PATCH 成功时的通用响应。GET 时附带资源表述,PUT/PATCH 可附带更新后的资源或空体。
  • 201 Created:POST 创建资源成功。务必在响应头 Location 字段中返回新资源的 URI
    HTTP/1.1 201 Created
    Location: /users/2048
    Content-Type: application/json
    
    { "id": 2048, "name": "Alice", ... }
    
  • 204 No Content:DELETE 成功或 PUT/PATCH 无需返回实体时的最佳选择,响应体为空。
  • 400 Bad Request:请求格式错误(如 JSON 解析失败)、参数缺失或业务校验失败。响应体中应给出具体错误信息。
  • 401 Unauthorized:认证缺失或凭据无效,需客户端提供合法的认证令牌。
  • 403 Forbidden:认证已通过,但当前身份无权执行该操作(权限不足)。
  • 404 Not Found:请求的 URI 不存在,或资源本身不存在(取决于设计策略)。当用户无权限查看某资源时,出于安全考虑,也常返回 404 而非 403,避免暴露资源存在性。
  • 409 Conflict:资源状态与请求产生业务冲突,例如对同一资源的并发修改,或创建已存在的唯一字段资源。
  • 422 Unprocessable Entity:语义正确,但数据逻辑校验失败(如年龄字段为负数)。常用于比 400 更细粒度的业务规则错误。
  • 500 Internal Server Error:服务器内部未预期异常,属于通用兜底错误。应避免将详细的堆栈信息暴露到响应体中。

错误响应体结构设计

为帮助客户端开发者快速定位问题,建议统一错误响应格式。一个高可用的结构通常包含错误码、消息和具体错误细节数组。

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The provided data is invalid.",
    "details": [
      { "field": "email", "issue": "format_invalid", "value": "not-an-email" },
      { "field": "age", "issue": "too_small", "min": 18 }
    ]
  }
}

资源表述与媒体类型

资源可以有多种表述形式(JSON、XML、HTML 等),客户端通过 Accept 请求头告知期望的格式,服务器通过 Content-Type 响应头明确返回的格式。JSON 已事实上成为 RESTful API 的主流格式。

  • 请求Accept: application/json
  • 响应Content-Type: application/json; charset=utf-8

设计中避免将所有响应包裹在一个固定结构如 { success: true, data: ... } 中,因为 HTTP 状态码已经承载了请求是否成功的信息。直接返回数据对象本身,能让接口更简洁。但对于需要元数据的集合资源(如分页信息),可提供信封结构:

{
  "data": [ ... ],
  "page": 2,
  "pageSize": 20,
  "totalCount": 156
}

版本管理策略

API 必然面临迭代变更。常见的版本控制方式有三种:

  1. URI 路径版本/v1/users。直观清晰,但容易产生大量重复路由。
  2. 请求头版本Accept: application/vnd.myapi.v1+json。最符合 REST 无侵入理念,但不够直观且浏览器调试不便。
  3. 查询参数版本/users?version=1。简单却容易污染 URI。

对于公开 API,URI 路径版本是最为开发者友好和普遍的选择;对于内部微服务,请求头版本配合 API 网关可能更灵活。无论选用哪种策略,均应保证旧版本在适当的弃用宽限期后平稳下线。

实战提示与常见陷阱

  • 不要过度设计:初学者不必完美遵循所有原则。从清晰的资源划分和正确的动词、状态码开始,剩下的原则在迭代中逐步引入。
  • HATEOAS 并非必选项:超媒体作为应用状态引擎(HATEOAS)是 REST 成熟度模型的最高级,但大部分实际项目并不实现它。不要为此束缚手脚。
  • 正确使用查询参数:查询参数适用于过滤、排序、分页,而不是资源的标识。/users?id=1024 从语义上并非表示单个资源,而是对 /users 集合进行 id 过滤后得到的单元素集合,应优先使用 GET /users/1024
  • 避免“万能 GET”:不要设计一个接受复杂 SQL 或相似参数的端点来满足所有查询需求,这会破坏缓存和自描述性。为每种查询场景定义清晰的资源模式。
  • 安全考虑:始终进行输入验证,防范 SQL 注入、XSS 等攻击。使用 HTTPS,保护数据在传输中的完整性。

掌握了资源、动词、状态码这三大支柱,你已经可以设计出可用的 RESTful API 了。在此基础上逐步应用缓存控制、身份认证、速率限制等更高级的主题,你将构建出健壮且让人愉悦的网络服务。