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 层次关系更清晰

命名约定

命名的一致性可以防止混淆并减少错误。遵循以下规则:

处理非资源操作

有时您需要暴露不能完全适应资源模型的操作。对于这些情况,将操作本身视为资源:

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):

客户端错误代码(4xx):

服务器错误代码(5xx):

专业提示:始终在 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"
  }
}

错误响应组件

验证错误最佳实践

一次返回所有验证错误,而不仅仅是遇到的第一个。开发者不应该玩打地鼠游戏,修复一个错误后才发现另一个。

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

常见模式: