设计可用、灵活、持久的 API

记得最后一次提交就像昨天一样,尽管那是几年前的事情。团队花了六个月时间构建应用程序,如今已经准备好发布。遵循了所有最佳实践,代码也足够完善,正好赶上了大发布的时机。然而,等待数字的过程中,客户的兴趣却消失了。

设计的解决方案并未满足客户的实际需求。恐慌随之而来,团队在接下来的三到四个星期内快速重构代码,试图调整为客户能够接受的版本,但为时已晚。最终,公司在一个被搁置的项目上耗费了约 100 万美元,这一切都源于忽视了一条简单的规则:构建 API 很容易,而设计一个可用、灵活、持久的 API 则困难重重。

这一经历虽然令人沮丧,但更让人惊讶的是,许多小型和大型企业也常常忽视这一简单原则。如果无法明确构建的内容、构建的目的以及目标用户是谁,又如何能真正满足他们的需求?如何能够成功呢?

确定 API 的目标和需求

在设计 API 之前,首先需要明确其目标用户:是针对内部消费者、客户、第三方开发者,还是以上所有?

明确目标后,下一个问题便是他们需要访问哪些内容。这一步常常与 HTTP 方法(如 GET、POST、PUT、PATCH)混淆,但在此处,应该重点列出他们需要执行的操作。

考虑用户需要的操作时,可以将其整理为如下表格:

虽然这个表格简单,却从一开始就提供了显著的优势。在构建 RESTful API 时,资源(如用户和留言)已得到确定。此外,该表格促使思考工作流程,并帮助理清端点的合理位置,减少重复。例如,若存在资源/messages,则不必在/users 下为消息用户提供端点,而是可以考虑在用户对象与消息之间建立关联,以便随时构建超媒体链接。

接下来的步骤是理解为何选择当前这种 API 类型。这一步至关重要,因为它迫使深入思考选择该格式的具体原因,并理解所选格式的优势。

大多数 API 并不是真正的 REST API,因此在选择构建 RESTful API 时,需要了解 REST 的限制(包括超媒体/HATEOAS)。如果选择构建部分 REST 或类似 REST 的 API,需明确为何不遵循某些 REST 的约束。

根据 API 的用途及使用场景,诸如 SOAP 等旧格式在某些情况下可能更适用,但每种格式都需要在可用性、灵活性和开发成本之间权衡。

在规划 API 时,了解用户如何与 API 交互及其如何与其他服务结合使用非常重要。在此过程中,务必使用如 RAML 或 Swagger/OAI 等工具,与用户互动,提供模拟 API 供测试,并确保设计的一致性和用户需求的满足。

最佳实践

在设计 API 时,务必要为未来的构建奠定坚实基础。杀死 API 最简单的方法之一就是试图重新发明轮子,或者在未经过充分测试的情况下改进现有标准或想法。

换句话说,作为开发人员,不应追求不切实际的想法,而是基于消费者对 API 的需求,采用经过验证的最佳实践。

名词与动词

在构建 RESTful 或类似 REST 的 API 时,应使用名词作为资源。例如,应使用 /users 并依赖 HTTP 方法,而不是使用 /getUser/deleteUser

一个更有争议的最佳实践是对返回集合的资源使用复数命名约定。即使资源当前仅返回单个项目,如果有可能返回多个项目,最好将其创建为集合。例如,如果有一个只返回一个地址的资源 /address,未来可能涉及多个地址(如账单地址和送货地址),从一开始应将资源命名为 /addresses 并将地址作为数组返回,以确保未来扩展时合约不变。

接受和内容类型标头

避免代价高昂的重构的一项最佳实践是利用接受和内容类型标头。无论 API 当前只接受 JSON 还是 XML,未来可能需要添加其他格式。

通过在一开始就构建上下文切换器,添加新格式变得非常简单,只需在后台添加合适的库并允许该格式类型即可,这样便可以轻松支持多种格式,而无需对 RESTful API 进行重构。

HTTP 方法的使用

由于 RESTful 或类似 REST 的 API 操作由 HTTP 方法决定,了解每个方法的作用及其使用场景非常重要。通常可以使用 CRUD 操作来理解这些方法:

  • 创建 — POST
  • 读取 — GET
  • 更新 — PUT、PATCH
  • 删除 — DELETE

虽然许多 HTTP 方法看似简单,但 RFC 标准允许每种方法都有独特的用法。例如,POST 可以用于大多数操作,但应优先用于在集合中创建新项目或新记录。尽管 POST 对集合有意义,但在项目级别可能不适用。

PUT 是另一种独特的方法,既可以用于更新现有记录,也可以在新记录不存在时创建新记录(前提是提供了显式 ID,记录不存在,并返回 201 状态)。需要注意的是,许多开发人员不熟悉 PUT 的创建用法,且许多框架不会测试是返回 200 还是 201。因此,开发人员可能在期望 404 NotFound 时收到成功的响应。因此,在使用 PUT 创建新记录时应非常谨慎,除非 API 中明确必要且有详细记录。

PATCH 被广泛推荐作为 PUT 的替代方案,用于部分更新数据记录。

此外,还需注意不要允许对集合进行 PUT、PATCH 或 DELETE 操作,因为这可能会导致更新或删除集合中的所有项目。

描述性错误消息

除了了解如何使用 API,了解使用 API 时为什么某些功能不起作用也至关重要。很多 API 的错误消息只是“出了问题”或“参数无效”,这样的信息对开发人员没有帮助。

相反,应提供清晰、相关的错误消息。不要只是告诉用户“ID 丢失了”,而是说明为何需要 ID。如果需要用户向团队发送支持请求,提供内部支持代码。同时,提供指向相关文档的链接,指导用户如何修复错误。无需重新发明轮子或猜测错误消息应包含的内容。

目前已有多种优秀的错误格式可供参考,包括 Google Errors、JSON API 和 vnd.error。

使用超媒体

虽然 HATEOAS(超媒体作为应用程序状态引擎)听起来复杂,但其实它的目标很简单:从客户端中移除业务逻辑。

通常情况下,API 构建方式是:客户端收到响应后,根据响应来判断可以采取的操作。客户端可能通过利用 OPTIONS 方法(如果 API 允许)或尝试请求以获取状态码(如 200 或 400)来确认某些操作的有效性,也可能由开发人员在客户端中硬编码业务逻辑,期望未来不会有变化。

HATEOAS 通过返回链接来告知客户端下一步可执行的操作。如果业务规则发生变化,返回的链接也会相应变化,从而使得客户端或开发者无需返工。这种机制的重要性在于,它使客户端和服务器能够独立发展。

例如,有三名用户:管理员、普通用户和因发送垃圾邮件被暂停的用户。每个用户的状态不同,超级用户不能删除,普通用户可以被暂停,而被暂停的用户不能再次被暂停。HATEOAS 通过响应中返回的链接以简明方式解释了每个用户的状态,客户端无需理解底层的业务规则。

超媒体的常见形式在互联网上随处可见,例如浏览 DZone.com 时,用户不需要输入特定文章的 URL,只需通过点击链接逐步进入目标页面,这一过程由 HTML(超文本标记语言)实现。

除了 HTML,当前 API 还可使用多种流行的超媒体格式,包括 HAL 和 JSON API。随着这些规范的成熟,更新的格式如 Siren 和 CPHL 也逐渐出现,通过 HTTP 方法、表单、按需代码和灵活文档等方式扩展了 HAL 格式。

HAL/CPHL 示例:

{
  "firstName": "Mike",
  "lastName": "Stowe",
  "_links": {
    "edit": {
      "href": "/users/1",
      "methods": [
        "put",
        "patch"
      ]
    },
    "message": {
      "href": "/message?to=1",
      "methods": [
        "post"
      ]
    }
  }
}

超媒体还允许合同的灵活性。假设某个用户意外地陷入无限循环,导致消息资源被发送了 10,000 次。为了解决这个问题,可以向资源添加一个令牌。如果资源是硬编码的,就会违反合同,必须对 API 进行版本控制,并要求所有客户端更新代码。

然而对于使用超媒体的 API,由于依赖键值对,可以灵活更改 URL 或添加令牌,而不会违反合同或给用户带来不便。例如,对上述响应的简单更改如下:

"message": {
  "href": "/message?to=1&token=893hdy83ikaherukaehfiwf7w48ika",
  "methods": ["post"]
}

这样就可以在不破坏 API 合同的情况下进行额外检查。

回顾一下

构建 API 很容易,但设计一个足够灵活、能够持续多年并随着平台发展而演进的 API 却十分困难,更不用说提升其可用性。这些最佳实践以及 RAML 或 Swagger/OAI 等工具可以帮助实现这一挑战。然而,必须花时间让用户参与,并仔细考虑 API 的每个方面。就像建造地基一样,一块石头放错位置可能会导致整个建筑倒塌,而在 API 设计中,只需一个小问题就可能显著缩短其使用寿命。

原文链接:Designing a Usable, Flexible, Long-Lasting API

Leave a Reply