【译】RESTful API 最佳实践

2025年02月12上次更新于 5 天前
翻译

原文:REST API Design Best Practices,本文内容为笔者翻译以及自我理解的分享。

fahim-muntashir-OqOhYRjn_JY-unsplash (1).jpg

核心原则

设计 API 应该:

  • 易读且易用
  • 难以误用(每一个接口都是明确的,无歧义)
  • 完整且简洁(不仅仅是项目的需求,还应该适当补全可能的需求,并且不失简洁)

名称和 URL 结构

资源名称

  • 使用名词代替资源,而非动词
    • Good:/items,/books
    • Bad:/createItems, /getBooks
  • 使用复数名称表示集合
    • /book/books
  • 使用破折号以提高可读性(例如,inventory-management)而不是下划线

URL 结构

  • 为嵌套资源实现逻辑分组
    • 例如:/customers/{id}/orders
  • 避免深层次的嵌套
    • 避免:/users/123/posts/456/comments/789/likes,此类难以看出资源之间的关系
  • 避免在 URL 中透露数据库结构,以免泄露不必要的信息(例如按序生成的 id 或浮标)

版本

  • 永远为接口提供版本开头的路由,以免在破坏性更新的时候增加处理逻辑的复杂性
  • 可选的思路
    • 地址版本:/v1/book/v2/book
    • 查询参数:?version=2

分页

  • 为大量的数据查询提供分页接口
  • 使用基于 Cursor(光标) 的分页机制(更高效,简单)
    • 基于 Cursor 的分页机制,使用一个唯一的标识符(如 lastItemId),来标识上一次请求返回的最后一项数据。接下来,服务器通过这个光标来获取下一批数据。
    • 这种分页方式避免了传统分页中可能遇到的跳过或重复数据的问题。比如,如果数据在两次分页请求之间发生变化,使用光标分页可以确保每次都从上一项数据之后继续获取,不会漏掉或者重复返回某些数据。
    • 举例: /items?lastItemId=1000&limit=20

筛选和排序

  • 允许通过 URL 查询参数进行筛选
    • 例子:/users?lastName=Smith&age=30
  • 支持动态选择数据的列
    • 例子:/products?fields=id,name,price (从数据库中读取少量数据,高效)
  • 使用清除参数提供排序逻辑
    • 例子:/posts?sort=+author,-datePublished

注:通过 URL 传输查询参数的方案不够方便,一来 URL 长度有限(依然够长,小问题),二来更重要的是查询参数的类型需要额外的机制去统一序列化,通常前端的数字通过此方式传输到接口层,后端需要处理字符串类型。相对通过 JSON Payload 传输需要额外的统一的序列化机制支持

API 操作

幂等性 (重点)

  • 在适当的情况下,确保操作是幂等的,即多次相同的请求应产生相同的结果。
  • 对于 DELETEPUT操作尤为重要。
  • 对于敏感操作,可以考虑使用幂等性键。例如,Stripe在收费操作中使用了幂等性键。

异步操作

  • 对于耗时较长的操作,使用状态码 202表示操作已被接受但尚未完成。
  • 提供状态端点用于跟踪进度,例如 GET /orders/123/status
  • Location头部中包含状态端点的URL,帮助客户端了解操作状态的获取位置。这遵循了HATEOAS原则(超媒体作为应用状态的引擎),即API应告知客户端下一步可以做什么。
  • 考虑支持操作取消,例如 DELETE /orders/123/cancel

部分响应

  • 支持对大型资源(如视频文件)的部分内容检索。这对于不想一次性下载整个文件的情况非常有用,例如Netflix的电影。
  • 客户端可以通过 HEAD请求检查资源大小,然后使用 Range头部请求特定的分块。(不错的技巧,检测资源而不需要获取资源)

安全性

  • 使用SSL/TLS加密,通过HTTPS保护数据传输。
  • 采用适当的认证和授权机制,如OAuth、JWT等。
  • 应用速率限制,以防止拒绝服务(DoS)攻击。
  • 谨慎处理错误消息,避免泄露敏感信息。应提供清晰的错误消息,说明问题所在以及下一步的操作。

错误处理

  • 返回适当的HTTP状态码,以准确反映错误类型。
  • 提供清晰的错误消息,帮助开发者理解问题。
  • 包含足够的调试信息,但同时避免暴露敏感细节。
  • 对于成功的空响应,使用状态码 204。(通常后端会返回 200 😂)

题外话,我曾经思考过前端请求中的拦截器在返回响应数据之前应该如何处理服务端状态码。 我认为应该统一使用规范的状态码,避免使用自定义状态码(在返回的 JSON 中定义 status code)。 在某次群组讨论中,有人提出应该在拦截器中既处理常规服务端异常状态码,也要在应用的返回 JSON 中提供应用级别的状态码。

转移一下话题,让 AI 来分析一下第二种做法:在 HTTP 响应中使用标准的 HTTP 状态码,同时在响应体中提供应用级状态码(如 data.code)有几个明显的好处:

1. 分离关注点,增强灵活性

HTTP 状态码负责协议级别的错误处理,例如,网络错误、未授权、服务器错误等,这些问题通常与请求的传输和处理过程有关。而 应用级状态码(如 data.code**)则专注于应用逻辑层的状态**,比如操作成功、数据验证失败、权限不足等。

• 通过分离这两者,你可以在 HTTP 协议层面保持标准化,同时在应用层提供更细粒度的控制。这使得前端能更精确地响应不同的应用级别错误,而不至于因为协议层的状态码而混淆业务逻辑。

2. 清晰的错误处理

• 使用标准的 HTTP 状态码可以保证大多数情况都能通过简单的检查处理。例如,4xx 状态码就能快速告诉你是否是客户端请求错误,5xx 则提示服务器故障。

• 但对于更复杂的错误或业务逻辑层的错误,应用级状态码可以提供更多信息,如表单字段错误、用户权限问题、请求失败的具体原因等。前端可以通过这些状态码精准控制显示错误信息或者执行相应的操作,而不必依赖于模糊的 HTTP 状态码。

3. 提升可扩展性

• 如果你的应用有较复杂的业务逻辑,使用标准的 HTTP 状态码可能无法涵盖所有业务场景。比如,一个登录接口可能返回 200 状态码,但需要根据 data.code 来判断是登录成功还是账号被锁定。

• 应用级状态码使得你能够定义更加细化的业务逻辑处理流程。随着应用规模增长,增加新的应用级状态码也会变得更加清晰和有序。

4. 用户体验的优化

• 前端可以通过 data.code 做出非常细粒度的用户提示和引导,避免过度依赖 HTTP 状态码。例如: • 200 状态码表示请求成功,但可以通过 data.code 判断是否需要用户做出进一步操作(如修改某些字段)。 • 401 状态码表示未授权,但 data.code 可能告诉用户“您的账户已过期”或“您的权限不足”。 • 这种细节上的处理能极大地提升用户体验,减少因信息不充分而导致的困惑。

5. 便于国际化与本地化

• 通过应用级状态码返回的错误信息可以与用户语言和地区相关联。你可以根据 data.code 和对应的消息字段来返回本地化的错误信息,而无需依赖 HTTP 状态码本身。

• 这种方式也能让你方便地为用户提供定制化的解决方案,例如,依据 data.code 提供不同的帮助文档链接或者操作指南。

6. 可调试性和日志记录

• 使用应用级状态码可以让开发者在日志中记录更具体的错误信息,有助于定位和排查问题。比如,可以在日志中明确标记是由于数据验证失败、权限问题还是某个特定业务条件未达成而导致的错误。

这样,你不仅能在后端更好地跟踪错误发生的原因,也能在前端做针对性的调试和处理。

我被说服了。

文档

文档是重中之重,提供设计文档甚至优先于提供测试接口

  • **使用OpenAPI(原Swagger)**进行API文档编写。
  • 文档内容应包括
    • 端点结构。
    • 请求和响应格式。
    • 认证要求。
    • 错误代码和消息。

HATEOAS (超媒体作为应用状态的引擎)

HATEOAS (Hypermedia as the Engine of Application State) 是一种 RESTful API 的设计理念,它是 REST 架构风格的一个重要特性之一。HATEOAS 强调“超媒体作为应用程序状态的引擎”,也就是说,客户端不仅能获取资源的数据,还能通过从资源响应中获得的超链接来进一步了解该资源及其操作。

HATEOAS 主要是通过返回动态生成的超链接(links)来引导客户端如何进行下一步的操作,而不仅仅是返回数据。客户端通过这些超链接可以根据当前资源状态,知道能够进行哪些操作。

举个例子: 假设我们有一个用户资源,客户端请求一个特定的用户(例如 /users/1),返回的响应可能如下:

{
  "id": 1,
  "name": "Alice",
  "email": "[email protected]",
  "links": [
    {
      "rel": "self",
      "href": "/users/1"
    },
    {
      "rel": "update",
      "href": "/users/1/update"
    },
    {
      "rel": "delete",
      "href": "/users/1/delete"
    },
    {
      "rel": "friends",
      "href": "/users/1/friends"
    }
  ]
}

在这个例子中:

• rel 属性描述了链接的关系,告诉客户端该链接所代表的操作是什么。例如,“self”表示获取当前资源的 URL,“update”表示更新用户信息的 URL。

• href 属性提供了实际的 URL,客户端可以根据这些链接进行进一步操作。

😄 长见识了,如上所示:传统的 REST API 可能要求客户端在请求中使用硬编码的 URL,或者客户端自己在多个请求之间维护操作顺序。而 HATEOAS 通过动态链接提供了明确的导航,使得客户端不需要依赖硬编码的 URL 结构。

我喜欢这种概念,非常灵活。

以上就是今天分享的内容,希望大家喜欢。

Bye.

not-by-ainot-by-ai
文章推荐

Friends

Jimmy老胡SubmaraBruce SongScarsu宇阳Steven Lynn's Blog