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
格式的字符串。在特定时区显示日期通常是客户端应用程序的关注点。
{
"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
状态码外,返回错误时,始终使用包含更详细信息的标准化错误响应。消费者始终可以期望相同的结构并相应地进行操作。
// 请求 => GET /users/4TL011ax
// 响应 <= 404 Not Found
{
"code": "user/not_found",
"message": "未找到 ID 为 4TL011ax 的用户。"
}
// 请求 => POST /users
{
"name": "John Doe"
}
// 响应 <= 400 Bad Request
{
"code": "user/email_required",
"message": "参数 [email] 是必需的。"
}
11. 在 POST 请求后返回已创建的资源
在使用 POST
请求创建资源后,将创建的资源返回是个好主意。这主要是因为返回的已创建资源将反映底层数据源的当前状态,并包含更近期的信息(如生成的ID
)。
// 请求: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
(或类似的)来控制想要检索的块。
// 请求 => 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
(或类似的)的查询参数加载相关数据。这对于避免往返并在一次请求中加载执行特定操作所需的所有数据尤其有用。
// 请求 => 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 订阅"
}
]
}
]
}
网友评论