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

常见模式: