【译】如何设计更好的API

API的设计非常困难。在从头开始创建 API 时,你需要注意许多细节。从基本的安全考虑到使用正确的 HTTP 方法,实施身份验证,决定应该接受和返回哪些请求和响应……

一个让你的用户喜欢使用的 API。所有的技巧都是与语言无关的,因此它们适用于任何框架或技术。

1. 保持一致

这听起来很合理,但要做到这一点很难。最好的 API 是可预测的。当用户使用和理解一个应用时,他们可以期望另一个应用以相同的方式工作。这对整个API非常重要,也是衡量一个 API 是否设计良好并且易于使用的关键指标之一。

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

2. 使用 ISO 8601 UTC 日期

在处理日期和时间时,API 应始终返回 ISO 8601 格式的字符串。在特定时区显示日期通常是客户端应用程序的关注点。

1
2
3
{
"published_at": "2022-03-03T21:59:08Z"
}

3. 为公共场景提供例外

默认情况下,每个接口都应需要授权。大多数接口需要经过身份验证的用户才能调用,因此将此设置为默认值是有道理的。如果一个接口需要公开调用,明确设置此接口以允许未经授权的请求。

4. 提供健康检查接口

提供一个接口(例如 GET /health),用于确定服务是否正常。其他应用程序(如负载均衡器)可以调用此接口以应对服务中断。

5. 对 API 进行版本管理

确保对 API 进行版本管理,并在每个请求中传递版本,以便消费者不受另一个版本的任何更改的影响。可以使用 HTTP 标头或查询/路径参数传递 API 版本。甚至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)。它们应该有一个过期日期,并且必须能够撤销活动密钥,以便在它们被 compromise 时可以使其失效。避免将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. 使用简明易懂的名称

大多数端点都是面向资源的,应该以这种方式命名。不要添加可以从其他地方推断出来的不必要信息。这也适用于字段名称。

👍 GOOD

  • GET /users => 检索用户
  • DELETE /users/{id} => 删除用户
  • POST /users/{id}/notifications => 为特定用户创建通知
  • user.first_name
  • order.number

👎 BAD

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

    10. 使用标准化的错误响应

    除了使用指示请求结果(成功或错误)的 HTTP 状态码外,返回错误时,始终使用包含更详细信息的标准化错误响应。消费者始终可以期望相同的结构并相应地进行操作。
1
2
3
4
5
6
7
// 请求 => GET /users/4TL011ax

// 响应 <= 404 Not Found
{
"code": "user/not_found",
"message": "未找到 ID 为 4TL011ax 的用户。"
}
1
2
3
4
5
6
7
8
9
10
// 请求 => POST /users
{
"name": "John Doe"
}

// 响应 <= 400 Bad Request
{
"code": "user/email_required",
"message": "参数 [email] 是必需的。"
}

11. 在 POST 请求后返回已创建的资源

在使用 POST 请求创建资源后,将创建的资源返回是个好主意。这主要是因为返回的已创建资源将反映底层数据源的当前状态,并包含更近期的信息(如生成的 ID)。

1
2
3
4
5
6
7
8
9
10
11
12
// 请求:POST /users
{
"email": "jdoe@averagecompany.com",
"name": "John Doe"
}

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

12. 优先使用 PATCH 而不是 PUT

如前所述,PATCH 请求应对资源进行部分更新,而 PUT 则完全替换现有资源。通常,围绕 PATCH 请求设计更新是个好主意,因为:

  • 当使用 PUT 仅更新资源的一部分字段时,仍然需要传递整个资源,这使得它更加网络密集且容易出错。
  • 允许任何字段在没有任何限制的情况下更新也是非常危险的。
  • 根据我的经验,在实践中几乎不存在任何使用案例,其中对资源进行完全更新是有意义的。
  • 想象一下,一个订单资源具有 id 和 state。
  • 允许消费者更新订单状态将非常危险。
  • 更可能由另一个端点触发状态更改(例如 /orders/{id}/fulfill)。

13. 尽可能具体

如前一节所述,通常在设计接口、命名字段和决定接受哪些请求和响应时尽可能具体是个好主意。如果 PATCH 请求仅接受两个字段(名称和描述),则不会错误使用它并破坏数据的危险。

14. 使用分页

对返回资源集合的所有请求进行分页,并使用相同的响应结构。使用 page_number page_size(或类似的)来控制想要检索的块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 请求 => GET /users?page_number=1&page_size=15

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

15. 允许扩展资源

允许消费者使用名为 expand(或类似的)的查询参数加载相关数据。这对于避免往返并在一次请求中加载执行特定操作所需的所有数据尤其有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 请求 => GET /users/T9hoBuuTL4?expand=orders&expand=orders.items

// 响应 <= 200 OK
{
"id": "T9hoBuuTL4",
"email": "jdoe@averagecompany.com",
"name": "John Doe",
"orders": [
{
"id": "Hy3SSXU1PF",
"items": [
{
"name": "API 课程"
},
{
"name": "iPhone 13"
}
]
},
{
"id": "bx1zKmJLI6",
"items": [
{
"name": "SaaS 订阅"
}
]
}
]
}