RESTful API 设计原则:资源、动词与状态码
理解 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 必然面临迭代变更。常见的版本控制方式有三种:
- URI 路径版本:
/v1/users。直观清晰,但容易产生大量重复路由。 - 请求头版本:
Accept: application/vnd.myapi.v1+json。最符合 REST 无侵入理念,但不够直观且浏览器调试不便。 - 查询参数版本:
/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 了。在此基础上逐步应用缓存控制、身份认证、速率限制等更高级的主题,你将构建出健壮且让人愉悦的网络服务。