REST API 设计最佳实践

资源命名 · 状态码 · 版本控制 · 错误处理全解析

好的 API 设计能让开发者用得爽、少踩坑、效率高。差的 API 设计则会让对接的人骂娘,文档写了一堆还是看不懂。REST 是目前最主流的 API 设计风格,但很多人所谓的 REST API 其实只是"HTTP 接口"——用了 URL 和 JSON,但完全没抓住 REST 的精髓。本文总结了 REST API 设计的最佳实践,从命名规范到错误处理,从版本控制到安全认证,帮你设计出专业优雅的 API。

1. 资源命名:REST 的核心

REST 的核心思想是资源。URL 应该代表资源(名词),而不是动作(动词)。HTTP 方法(GET/POST/PUT/DELETE)才是动作。

// ❌ 不好的设计(动词 + 动作)
GET  /getUsers
POST /createUser
POST /updateUser?id=123
GET  /deleteUser?id=123

// ✅ 好的设计(名词复数 + HTTP方法)
GET    /users          // 获取用户列表
GET    /users/123      // 获取单个用户
POST   /users          // 创建用户
PUT    /users/123      // 全量更新用户
PATCH  /users/123      // 部分更新用户
DELETE /users/123      // 删除用户

命名原则

2. HTTP 方法与状态码的正确使用

REST API 应该充分利用 HTTP 协议本身的语义,而不是把所有请求都变成 POST + 自定义错误码。

方法语义

GET     // 读取资源,安全且幂等
POST    // 创建资源,非幂等
PUT     // 全量更新资源,幂等
PATCH   // 部分更新资源,非幂等(但通常当幂等用)
DELETE  // 删除资源,幂等
HEAD    // 获取资源元信息(和GET一样但不返回body)
OPTIONS // 获取资源支持的方法和能力

什么是幂等?执行一次和执行 N 次效果相同。比如 PUT /users/123,不管调用多少次,用户 123 的最终状态都是你设置的那样。而 POST /users 调用多次会创建多个用户,所以非幂等。

状态码使用规范

200 OK           // GET/PUT/PATCH 成功
201 Created      // POST 创建成功(响应体包含新资源)
204 No Content   // DELETE 成功(无响应体)
400 Bad Request  // 请求参数错误
401 Unauthorized // 未认证
403 Forbidden    // 已认证但无权限
404 Not Found    // 资源不存在
409 Conflict     // 资源冲突(如重复创建、乐观锁失败)
422 Unprocessable Entity // 语义错误(格式对但内容错)
429 Too Many Requests   // 限流
500 Internal Server Error // 服务端错误

3. 统一响应格式

统一的响应格式让前端对接更简单,减少心智负担。推荐格式:

// 成功响应
{
  "code": 0,
  "message": "success",
  "data": {
    "id": 123,
    "name": "张三"
  }
}

// 列表响应
{
  "code": 0,
  "message": "success",
  "data": {
    "items": [ ... ],
    "total": 100,
    "page": 1,
    "pageSize": 20,
    "totalPages": 5
  }
}

// 错误响应
{
  "code": 40001,
  "message": "用户名已存在",
  "details": [
    { "field": "username", "message": "该用户名已被注册" }
  ],
  "requestId": "req_abc123"
}

code 字段建议区分 HTTP 状态码和业务错误码。HTTP 状态码表示协议层面的结果,业务 code 表示具体的业务错误(如 40001=用户名已存在、40002=密码错误)。

4. 分页、过滤与排序

列表接口一定要支持分页。常见的分页方式有两种:

// 偏移量分页(简单但深分页性能差)
GET /users?page=1&pageSize=20
GET /users?offset=0&limit=20

// 游标分页(性能更好,适合大数据量)
GET /users?limit=20&cursor=eyJpZCI6MTAwfQ

过滤和排序的设计:

// 多条件过滤
GET /users?status=active&role=admin

// 范围查询
GET /orders?createdAt[gte]=2024-01-01&createdAt[lte]=2024-01-31

// 排序(- 表示降序)
GET /users?sort=-createdAt,name
// 按创建时间降序,同时间按姓名升序

// 字段选择(只返回需要的字段,减少传输)
GET /users?fields=id,name,email

5. 版本控制

API 一旦发布,修改就要非常谨慎。破坏性变更必须通过新版本发布。常见的版本控制方案有三种:

// 1. URL 路径版本(最直观,推荐)
GET /api/v1/users
GET /api/v2/users

// 2. 查询参数版本
GET /api/users?version=2

// 3. Header 版本(更 RESTful 但不够直观)
GET /api/users
Accept: application/vnd.example.v2+json

URL 版本虽然不够"纯 REST",但最易理解和调试,也是业界最常用的方案。Stripe、GitHub 等大厂都使用这种方式。

6. 错误处理与调试支持

好的错误信息应该告诉开发者三件事:出了什么错、为什么出错、怎么修复。

开发环境可以额外返回 stack trace 方便调试,但生产环境必须关闭,避免泄露服务器信息。

7. 安全与认证

认证方式

// Bearer Token(JWT 最常用)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

// API Key(适合服务端集成)
X-API-Key: sk_live_abc123

// Basic Auth(简单但不安全,仅限内部或测试)
Authorization: Basic dXNlcjpwYXNz

安全最佳实践

8. 文档与测试

再完美的 API,没有好文档也会让人痛苦。API 文档应该包含:

推荐工具:OpenAPI/Swagger 是 API 文档的事实标准;Postman 适合手动测试;Hoppscotch 是开源的轻量级 API 测试工具。自动生成文档的方案:代码注释生成(JSDoc + swagger-jsdoc)、代码优先(TypeScript 类型生成 OpenAPI)、设计优先(先写 OpenAPI 再生成代码)。

9. HTTP 缓存设计

合理设计 API 的缓存策略,可以大幅减少服务端压力,提升客户端响应速度。REST API 应该充分利用 HTTP 缓存机制。

缓存头的选择

// 强缓存:浏览器直接使用本地缓存,不发请求
Cache-Control: max-age=3600  // 缓存 1 小时
Cache-Control: public, max-age=86400  // 公开缓存,1 天

// 协商缓存:发请求验证资源是否变化
ETag: "abc123def456"  // 资源版本标识
Last-Modified: Wed, 21 Aug 2024 10:00:00 GMT

// 不缓存
Cache-Control: no-cache  // 每次都验证
Cache-Control: no-store  // 完全不缓存

API 缓存策略建议

10. 幂等性设计

网络是不可靠的,请求可能超时、可能重复发送。幂等性保证同一个请求执行一次和执行多次效果相同,是分布式系统的重要设计原则。

// 使用 Idempotency-Key 保证幂等
// 客户端每次请求生成一个唯一 key
POST /payments
Idempotency-Key: uuid-v4-key
Content-Type: application/json

{
  "amount": 100,
  "orderId": "ORD123"
}

// 服务端逻辑:
// 1. 根据 idempotency key 查找是否已有记录
// 2. 有记录:直接返回之前的结果,不重复处理
// 3. 没记录:正常处理并保存结果和 key 的关联

支付、下单、转账这类操作,幂等性至关重要。Stripe 等支付平台都要求使用 Idempotency-Key。除了自定义 Header,也可以用业务字段实现幂等,比如订单号、流水号等唯一业务标识。

11. 分页与大数据量处理

当数据量很大时,普通的偏移量分页会有性能问题——offset 越大,查询越慢。游标分页是更好的选择。

// 偏移量分页(简单但深分页慢)
GET /articles?page=100&pageSize=20
// 后端:SELECT * FROM articles ORDER BY id LIMIT 20 OFFSET 1980;

// 游标分页(性能稳定)
GET /articles?limit=20&cursor=eyJpZCI6MjAwfQ
// 后端:SELECT * FROM articles WHERE id > 200 ORDER BY id LIMIT 20;

// 响应中返回下一页游标
{
  "items": [...],
  "nextCursor": "eyJpZCI6MjIwfQ==",  // 编码后的游标
  "hasMore": true
}

游标分页的缺点是只能顺序翻页,不能跳转到指定页。但对于无限滚动(Infinite Scroll)的场景,游标分页是完美选择。大多数移动端 App 的 feed 流都采用这种方式。

12. 国际化与本地化

如果你的 API 服务全球用户,需要考虑多语言、多时区、多货币等问题。

// 通过 Header 指定语言和时区
Accept-Language: zh-CN, en;q=0.9
X-Timezone: Asia/Shanghai
X-Currency: CNY

// 或者通过 query 参数
GET /api/users?lang=zh-CN&timezone=Asia/Shanghai

错误信息、日期格式、数字格式都应该根据用户的语言和地区来本地化。返回的时间统一使用 ISO 8601 + UTC 格式,客户端根据用户时区转换显示。货币一定要带单位和代码(如 "amount": 100, "currency": "CNY"),否则数字本身没有意义。

13. 速率限制(Rate Limiting)

限流是保护 API 不被滥用的重要手段。常见的限流算法有:

// 标准的限流响应头
X-RateLimit-Limit: 100        // 时间窗口内的请求上限
X-RateLimit-Remaining: 42     // 剩余请求数
X-RateLimit-Reset: 1692672000 // 限流重置的时间戳(Unix 时间)
Retry-After: 60               // 触发限流后,多少秒后可以重试

// 触发限流时返回 429 状态码
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "message": "请求过于频繁,请稍后再试",
  "retryAfter": 60
}

14. API 设计检查清单

设计完 API 后,对照这个清单检查一下:

  1. URL 使用名词复数,没有动词?
  2. HTTP 方法语义正确(GET 读取、POST 创建、PUT 全量更新、PATCH 部分更新、DELETE 删除)?
  3. 状态码使用恰当(2xx/3xx/4xx/5xx 各归其位)?
  4. 响应格式统一(code/message/data 结构)?
  5. 列表接口有分页、过滤、排序?
  6. 错误信息具体明确,有错误码和 requestId?
  7. 有版本控制策略?
  8. 认证方式安全(HTTPS + Bearer Token)?
  9. 有限流机制?
  10. 有完整的 API 文档?
  11. GET 请求是幂等的?
  12. 写入操作考虑了幂等性?
  13. 时间使用 ISO 8601 格式?
  14. 有缓存策略(ETag/Cache-Control)?
  15. CORS 配置安全合理?

好的 API 设计需要时间和经验的积累,但只要遵循这些基本原则,就能设计出专业、优雅、好用的 API。记住:API 是给人用的,不是给机器用的。让调用你的 API 的开发者感到舒服,就是最好的设计。

总结

API 设计是一门平衡的艺术——要符合 REST 规范,也要兼顾实际开发效率;要灵活强大,也要简单易用。记住几个核心原则:以资源为中心、充分利用 HTTP 语义、保持一致性、为开发者着想。好的 API 应该是"自解释"的——开发者看一眼 URL 就知道是什么资源,看 HTTP 方法就知道做什么操作,看状态码就知道结果。