REST API 设计最佳实践:构建开发者喜爱的 API
· 12 分钟阅读
📑 目录
设计良好的 API 使用起来令人愉悦。设计糟糕的 API 会造成挫败感、错误和支持工单,消耗团队资源。
随着 API 成为现代软件架构的支柱——连接微服务、移动应用、第三方集成和 AI 代理——正确的设计从未如此重要。成功的 API 与被开发者抛弃的 API 之间的区别不仅仅在于功能。它关乎可预测性、一致性和开发者体验。
这份综合指南涵盖了将优秀 API 与平庸 API 区分开来的实践,提供真实世界的示例和您今天就可以实施的可行建议。
REST API 基础
REST(表述性状态转移)是一种架构风格,而不是严格的协议。理解其核心原则有助于您在整个 API 开发过程中做出更好的设计决策。
REST 架构的六个指导约束是:
- 客户端-服务器分离:客户端和服务器独立运行,允许各自独立演进
- 无状态通信:每个请求包含所有必要信息;服务器在请求之间不存储客户端上下文
- 可缓存响应:响应明确指示是否可以缓存以提高性能
- 统一接口:资源在请求中被识别,客户端通过表述操作资源
- 分层系统:客户端无法判断它是直接连接到终端服务器还是中间服务器
- 按需代码(可选):服务器可以通过传输可执行代码来扩展客户端功能
在实践中,REST API 语义化地使用 HTTP 方法:GET 检索数据,POST 创建资源,PUT 替换整个资源,PATCH 部分更新,DELETE 删除资源。URL 将资源表示为名词,而不是将动作表示为动词。
专业提示:无状态性通常是最难维护的原则。避免在服务器上存储会话数据。相反,使用包含所有必要身份验证和授权信息的令牌(如 JWT)。
URL 设计:资源和命名
您的 URL 结构是开发者在探索您的 API 时遇到的第一件事。直观、可预测的 URL 减少认知负担,使您的 API 更易于学习和记忆。
面向资源的设计
将您的 API 视为暴露资源(名词)而不是动作(动词)。HTTP 方法指示动作,因此您的 URL 应该只标识您要操作的对象。
| 好 ✅ | 坏 ❌ | 原因 |
|---|---|---|
GET /users |
GET /getUsers |
HTTP 方法已经暗示"获取" |
GET /users/123 |
GET /user?id=123 |
资源标识符属于路径 |
POST /users |
POST /createUser |
HTTP 方法暗示"创建" |
DELETE /users/123 |
POST /deleteUser/123 |
使用正确的 HTTP 方法 |
GET /users/123/orders |
GET /getUserOrders?userId=123 |
层次关系更清晰 |
命名约定
命名的一致性可以防止混淆并减少错误。遵循以下规则:
- 集合使用复数名词:
/users、/products、/orders - 使用小写加连字符:
/user-profiles,而不是/userProfiles或/user_profiles - 逻辑嵌套相关资源:
/users/123/orders/456 - 限制嵌套深度:超过 2-3 层,使用查询参数或单独的端点
- 避免文件扩展名:
/users,而不是/users.json(改用 Accept 头) - 使用资源 ID,而不是数据库 ID:考虑为面向公众的标识符使用 UUID 或 slug
处理非资源操作
有时您需要暴露不能完全适应资源模型的操作。对于这些情况,将操作本身视为资源:
POST /users/123/password-reset
POST /orders/456/cancellation
POST /reports/generate
GET /search?q=laptop&category=electronics
这些端点表示动作或过程,当替代方案是强制使用笨拙的资源映射时,这是可以接受的。
快速提示:在设计 URL 时,想象向新开发者解释它们。如果您需要超过一句话来解释为什么 URL 以某种方式构建,那么它可能太复杂了。
HTTP 方法和状态码
HTTP 提供了丰富的词汇来描述操作。正确使用方法和状态码使您的 API 可预测,更易于缓存、调试和与标准 HTTP 工具集成。
HTTP 方法
| 方法 | 动作 | 成功代码 | 幂等 | 安全 |
|---|---|---|---|---|
GET |
检索资源 | 200 OK | 是 | 是 |
POST |
创建新资源 | 201 Created | 否 | 否 |
PUT |
替换整个资源 | 200 OK / 204 No Content | 是 | 否 |
PATCH |
部分更新 | 200 OK | 否* | 否 |
DELETE |
删除资源 | 204 No Content | 是 | 否 |
HEAD |
仅获取头部 | 200 OK | 是 | 是 |
OPTIONS |
获取允许的方法 | 200 OK | 是 | 是 |
*PATCH 可以设计为幂等的,但规范不保证
理解幂等性
幂等操作无论执行多少次都会产生相同的结果。这个属性对于分布式系统中的可靠性至关重要,因为网络故障可能导致重试。
GET /users/123 是幂等的——调用一次或 100 次都返回相同的用户数据。DELETE /users/123 也是幂等的——第一次调用删除用户,后续调用返回 404,但最终状态是相同的。
POST /users 不是幂等的——每次调用都会创建一个新用户。如果您需要幂等创建,请使用带有客户端生成 ID 的 PUT 或实现幂等键。
基本状态码
不要只使用 200 和 500。正确的状态码帮助客户端正确处理响应,而无需解析响应体。
成功代码(2xx):
200 OK— 标准成功响应,带响应体201 Created— 资源创建成功(包含 Location 头)204 No Content— 成功但无响应体(DELETE 常用)202 Accepted— 请求已接受进行异步处理
客户端错误代码(4xx):
400 Bad Request— 无效的请求语法或验证失败401 Unauthorized— 需要身份验证或验证失败403 Forbidden— 已验证但未授权404 Not Found— 资源不存在409 Conflict— 请求与当前状态冲突(例如,重复的电子邮件)422 Unprocessable Entity— 格式正确的请求上的验证错误429 Too Many Requests— 超过速率限制
服务器错误代码(5xx):
500 Internal Server Error— 通用服务器错误502 Bad Gateway— 来自上游服务器的无效响应503 Service Unavailable— 临时过载或维护504 Gateway Timeout— 上游服务器超时
专业提示:始终在 429 和 503 响应中包含 Retry-After 头,告诉客户端何时可以重试。这可以防止服务恢复时的惊群问题。
错误响应设计
错误响应是许多 API 不足的地方。隐晦的错误消息可以将 5 分钟的修复变成数小时的调试。您的错误响应应该是一致的、信息丰富的和可操作的。
标准错误格式
在所有错误响应中使用一致的结构:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求包含无效数据",
"details": [
{
"field": "email",
"message": "电子邮件地址已注册",
"code": "DUPLICATE_EMAIL"
},
{
"field": "password",
"message": "密码必须至少 8 个字符",
"code": "PASSWORD_TOO_SHORT"
}
],
"request_id": "req_7f8a9b2c3d4e5f6g",
"documentation_url": "https://api.example.com/docs/errors/validation"
}
}
错误响应组件
- 机器可读代码:使用客户端可以程序化处理的一致错误代码
- 人类可读消息:适合向最终用户显示的清晰解释
- 字段级详细信息:对于验证错误,指定哪些字段失败以及原因
- 请求 ID:包含用于支持和调试的唯一标识符
- 文档链接:指向相关文档以获取解决步骤
验证错误最佳实践
一次返回所有验证错误,而不仅仅是遇到的第一个。开发者不应该玩打地鼠游戏,修复一个错误后才发现另一个。
POST /users
{
"email": "invalid-email",
"password": "123",
"age": -5
}
响应:422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求验证失败",
"details": [
{
"field": "email",
"message": "必须是有效的电子邮件地址",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "必须至少 8 个字符",
"code": "TOO_SHORT"
},
{
"field": "age",
"message": "必须是正数",
"code": "INVALID_VALUE"
}
]
}
}
安全考虑
小心不要在错误消息中泄露敏感信息。不要透露用户账户是否存在,暴露内部系统详细信息,或在生产环境中提供堆栈跟踪。
不要使用:"未找到用户 [email protected]"
使用:"电子邮件或密码无效"
在服务器端记录详细的错误信息,但向客户端返回经过清理的消息。
分页和过滤
在单个响应中返回数千条记录会降低性能并造成糟糕的用户体验。对于任何返回集合的端点,分页都是必不可少的。
分页策略
基于偏移量的分页简单且熟悉:
GET /users?limit=20&offset=40
响应:
{
"data": [...],
"pagination": {
"limit": 20,
"offset": 40,
"total": 1247,
"has_more": true
}
}
优点:易于实现,支持跳转到任意页面
缺点:大偏移量性能下降,如果请求之间数据发生变化,结果不一致
基于游标的分页对于大型数据集更稳健:
GET /users?limit=20&cursor=eyJpZCI6MTIzNDU2fQ
响应:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIzNDc2fQ",
"has_more": true
}
}
优点:结果一致,性能更好,处理实时数据
缺点:无法跳转到任意页面,实现稍微复杂
基于页面的分页对 UI 用户友好:
GET /users?page=3&per_page=20
响应:
{
"data": [...],
"pagination": {
"page": 3,
"per_page": 20,
"total_pages": 63,
"total_items": 1247
}
}
过滤和排序
允许客户端使用查询参数过滤和排序结果:
GET /users?status=active&role=admin&sort=-created_at,name
常见模式:
- 相等:
?status=active - 比较:
?age_gt=18&age_lt=65