15个与语言无关的REST API设计技巧 - bluethl


在这篇文章中,我正在尽我最大的努力压缩我所知道的关于什么是好的 API 的一切。您的消费者会喜欢使用的 API。所有提示都与语言无关,因此它们适用于任何框架或技术。
 
1. 保持一致

  • 对字段、资源和参数使用相同的大小写(我更喜欢snake_case)
  • 使用复数或单数资源名称(我更喜欢复数)
    • /users/{id},/orders/{id}或/user/{id},/order/{id}
  • 对所有端点使用相同的身份验证和授权方法
  • 在 API 中使用相同的 HTTP 标头
    • 例如Api-Key用于传递 API 密钥
  • 根据响应类型使用相同的 HTTP 状态代码
    • 例如404,当找不到资源时
  • 对相同类型的操作使用相同的 HTTP 方法
    • 例如DELETE删除资源时

 
2. 使用ISO 8601 UTC 日期
在处理日期和时间时,API 应始终返回 ISO 8601 格式的字符串。在特定时区显示日期通常是客户端应用程序关心的问题。
{
    "published_at": "2022-03-03T21:59:08Z"
}

 
3. 对公共端点进行例外处理
默认情况下,每个端点都应该需要授权。大多数端点都需要调用经过身份验证的用户,因此将其设为默认值是有意义的。如果需要公开调用端点,请显式设置此端点以允许未经授权的请求。
 
4.提供健康检查端点
GET /health提供一个确定服务是否健康的端点(例如)。其他应用程序(例如负载均衡器)可以调用此端点以在服务中断时采取行动。
 
5. 版本化 API
确保对 API 进行版本控制并在每个请求中传递版本,这样消费者就不会受到对另一个版本的任何更改的影响。API 版本可以使用 HTTP 标头或查询/路径参数传递。即使是 API 的第一个版本(1.0)也应该明确地进行版本控制。
一些例子:
  • https://api.averagecompany.com/v1/health
  • https://api.averagecompany.com/health?api_version=1.0

 
6.接受API密钥认证
如果 API 需要由第三方调用,则允许通过 API 密钥进行身份验证是有意义的。API 密钥应使用自定义 HTTP 标头(例如Api-Key)传递。它们应该有一个到期日期,并且必须可以撤销活动密钥,以便在它们受到损害时可以使它们失效。避免将 API 密钥签入源代码控制(改用环境变量)。
 
7. 使用合理的 HTTP 状态码
使用传统的 HTTP 状态代码来指示请求的成功或失败。不要使用太多,并在整个 API 中为相同的结果使用相同的状态代码。一些例子:
  • 200取得普遍成功
  • 201为成功创作
  • 400对于来自客户端的错误请求
  • 401对于未经授权的请求
  • 403缺少权限
  • 404对于缺少的资源
  • 429对于太多的请求
  • 5xx对于内部错误(应不惜一切代价避免这些错误)

  
8.使用合理的HTTP方法
HTTP 方法有很多,但最重要的是:
  • POST用于创建资源
    • POST /users
  • GET用于阅读资源(单个资源和集合)
    • GET /users
    • GET /users/{id}
  • PATCH用于对资源应用部分更新
    • PATCH /users/{id}
  • PUT用于对资源应用完整更新(替换当前资源)
    • PUT /users/{id}
  • DELETE用于删除资源
    • DELETE /users/{id}

  
9.使用不言自明、简单的名字
大多数端点都是面向资源的,应该这样命名。不要添加可以从其他地方推断出的不必要的信息。这也适用于字段名称。
好:
  • GET /users=> 检索用户
  • DELETE /users/{id}=> 删除用户
  • POST /users/{id}/notifications=> 为特定用户创建通知
  • user.first_name
  • order.number

坏:
  • GET /getUser
  • POST /updateUser
  • POST /notification/user
  • order.ordernumber
  • user.firstName

 
10. 使用标准化的错误响应
除了使用指示请求结果(成功或错误)的 HTTP 状态代码外,在返回错误时,始终使用标准化的错误响应,其中包含有关问题的更详细信息。消费者总是可以期待相同的结构并采取相应的行动。
// Request => GET /users/4TL011ax

// Response <= 404 Not Found
{
    "code": "user/not_found",
    "message": "A user with the ID 4TL011ax could not be found."
}
// Request => POST /users
{
    "name": "John Doe"
}

// Response <= 400 Bad Request
{
    "code": "user/email_required",
    "message": "The parameter [email] is required."
}

 
11. 返回创建的资源POST
POST在使用请求创建资源后返回创建的资源是个好主意。这一点很重要,因为返回的创建资源将反映底层数据源的当前状态,并将包含更新的信息(例如生成的 ID)。
// Request: POST /users
{
    "email": "jdoe@averagecompany.com",
    "name": "John Doe"
}

// Response
{
    "id": "T9hoBuuTL4",
    "email": "jdoe@averagecompany.com",
    "name": "John Doe"
}

  
12. PATCH好于PUT
如前所述,PATCH请求应该对资源应用部分更新,而PUT完全替换现有资源。
而围绕 PATCH 请求设计更新通常是个好主意,因为:
  • 当使用PUT只更新资源的一部分字段时,仍然需要传递整个资源,这使得它更加网络密集且容易出错
  • 允许任何字段不受任何限制地更新也是相当危险的
  • 根据我的经验,在实践中几乎不存在任何对资源进行完整更新有意义的用例
  • 想象一个order资源有一个id和一个state
  • 允许消费者更新state一个order
  • 状态更改更有可能由另一个端点(例如/orders/{id}/fulfill)触发

 
13. 尽可能具体
如上一节所述,在设计端点、命名字段以及决定接受哪些请求和响应时,尽可能具体是一个好主意。如果一个PATCH请求只接受两个字段 (name和description),则不存在错误使用它和损坏数据的危险。
 
14.使用分页
对所有返回资源集合并使用相同响应结构的请求进行分页。使用page_numberand page_size(或类似的)来控制要检索的块。
// Request => GET /users?page_number=1&page_size=15

// Response <= 200 OK
{
    "page_number": 1,
    "page_size": 15,
    "count": 378,
    "data": [
        // resources
    ],
    "total_pages": 26,
    "has_previous_page": true,
    "has_next_page": true
}

 
15.允许扩展资源
允许消费者使用名为expand(或类似的)查询参数加载相关数据。这对于避免往返和一次性加载特定操作所需的所有数据特别有用。
// Request => GET /users/T9hoBuuTL4?expand=orders&expand=orders.items

// Response <= 200 OK
{
  "id": "T9hoBuuTL4",
  "email": "jdoe@averagecompany.com",
  "name": "John Doe",
  "orders": [
    {
      "id": "Hy3SSXU1PF",
      "items": [
        {
          "name": "API course"
        },
        {
          "name": "iPhone 13"
        }
      ]
    },
    {
      "id": "bx1zKmJLI6",
      "items": [
        {
          "name": "SaaS subscription"
        }
      ]
    }
  ]
}