重大变化不好! API 版本控制好!
任何构建或定期使用 API 的人早晚会意识到,重大变化是非常糟糕的,会对一个原本有用的 API 造成严重的污点。重大变化是指对 API 行为的更改,可能会破坏用户的集成,导致大量的挫折感和 API 提供者与用户之间信任的丧失。重大变化需要提前通知用户(并附上相应的歉意),而不是临时出现的变化,比如一个让人愉快的新特性。避免这种挫折感的方式是为 API 进行版本控制,并由 API 拥有者保证在任何单一版本内不会引入惊讶的更改。
那么版本控制 API 有多难呢?真相是这并不难,但难的是保持一些理智,不要无谓地陷入数不清的版本和子版本,这些版本和子版本在数十个 API 端点之间应用时其兼容性并不明确。
我们在三年前推出了 API 的 v1,没想到它至今仍然运作良好。那么,我们是如何在两年多的时间里继续提供最佳的电子邮件投递 API,同时仍保持同一版本的 API 的呢?虽然关于如何为 REST API 进行版本控制有很多 不同的看法 ,但我希望我们 humble yet powerful v1 的故事能够为您指明 API 版本控制的启蒙之路。
REST 是最好的
SparkPost API 起源于我们还是 Message Systems 的时候,那时我们正在为 Momentum 4 的 beta 发布做最后的准备。这是对版本 3.x 的重大升级,我们的市场领先的本地 MTA。Momentum 4 包括全新的 UI、实时分析,最重要的是一组用于消息注入和生成、管理模板和获取电子邮件指标的新 Web API。我们的愿景是 API 优先架构 - 甚至 UI 也会与 API 端点交互。
我们做出的最早且最好的决定之一就是采用 RESTful 风格。自 2000 年代后期以来,基于表现性状态转移 (REST) 的 Web API 已成为云 API 的事实标准。使用 HTTP 和 JSON 能让开发者轻松集成我们的 API,无论他们使用什么编程语言 - PHP、Ruby 和 Java - 而不需要知道或在乎我们的底层技术。
选择使用 RESTful 体系结构很简单。选择一个版本控制约定则没有那么简单。最初,我们在版本控制的问题上采取了回避策略,完全不对 beta 进行版本控制。然而,几个月后,beta 已经在少数客户手中,我们开始构建我们的云服务。 是时候进行版本控制了。我们评估了两种版本控制约定。第一种是在 URI 中直接放置版本号,第二种是使用 Accept 头。第一种选项更明确且不太复杂,对开发者来说更容易。 由于我们喜欢开发者,这是一个逻辑选择。
API 管理
选定版本控制约定后,我们还有更多问题。当我们提升版本时?什么算是重大变化? 我们是重新版本整个 API,还是仅对某些端点进行版本控制?在 SparkPost,我们有多个团队在 API 的不同部分工作。在这些团队内,人们在不同时间工作于不同的端点。因此,我们的 API 在使用约定时保持一致性是非常重要的。这不仅仅是版本控制。
我们建立了一个治理小组,包括每个团队的工程师、产品管理团队的一名成员以及我们的 CTO。该小组负责建立、记录和执行我们所有团队的 API 约定。一个 API 管理的 Slack 渠道也方便了就这一主题展开热烈讨论。
治理小组确定了一些可以引入对用户有益、但不构成重大变化的更改方式。这些包括:
一个新的资源或 API 端点
一个新的可选参数
对非公开 API 端点的更改
JSON POST 正文中的新可选键
JSON 响应正文中返回的新键
相对而言,重大变化包括任何可能破坏用户集成的内容,例如:
一个新的必需参数
POST 正文中的一个新的必需键
移除一个现有的端点
移除一个现有的端点请求方法
API 调用的实质性不同的内部行为 - 例如对默认行为的更改。
大版本 1.0
在我们记录和讨论这些约定时,我们也得出了结论,避免对 API 进行重大变化符合所有人的最佳利益(包括我们自己!)。因为管理多个版本会增加相当大的开销。我们决定在承诺“v1”之前应该修复 API 中的几个问题。
发送简单电子邮件需要付出过多的努力。 为了“保持简单的事情简单”,我们更新了 POST 正文,以确保能够兼容简单和复杂的用例。 新的格式也更具未来保障。 其次,我们解决了 Metrics 端点的问题。该端点使用了一个“group_by”参数,该参数会更改 GET 响应正文的格式,使得第一个键成为 group by 参数的值。这看起来并不太 RESTful,因此我们将每个 group by 拆分为一个单独的端点。最后,我们审计了每个端点,并进行了小的修改,以确保它们符合标准。
准确的文档
拥有准确和可用的 API 文档是非常重要的,以避免重大变化,无论是故意还是无意的。我们决定采用一种简单的 API 文档方法,利用一种称为 API Blueprint 的 Markdown 语言,并在 Github 上管理我们的文档。我们的社区贡献并改进这些开源文档。 我们还在 Github 中维护一套非公开的文档,用于内部 API 和端点。
最初,我们将文档发布到 Apiary,这是一个很好的原型和发布 API 文档的工具。然而,将 Apiary 嵌入到我们的网站在移动设备上无法使用,因此我们现在使用 Jekyll 生成静态文档。 我们最新的 SparkPost API 文档 现在加载快速,并在移动设备上表现良好,这对不总是坐在电脑前的开发者非常重要。
将部署与发布分开
我们早期学习到了分开部署和发布的重要技巧。这样可以通过持续交付和部署在准备好时频繁部署更改,但我们并不总是同时公开宣布或记录这些更改。我们经常会先在 UI 或内部工具中使用新 API 端点或现有 API 端点的增强功能,然后再公开记录和支持它。这样,我们可以对其进行一些用户友好的调整或标准符合性调整,而不必担心产生令人不快的重大变化。一旦我们对更改感到满意,我们会将其添加到我们的公开文档中。
哎呀!
不得不承认,我们确实有些时候未能达到我们的“没有重大变化”的理想,值得从中吸取教训。在一次情况下,我们决定如果某个属性默认为 true 而不是 false,那么对用户会更好。在我们进行更改后,收到了用户的几条投诉,因为行为发生了意外的变化。 我们撤回了更改,并添加了帐户级设置 - 这无疑是一个更人性化的方法。
我们偶尔会因为错误修复而被诱惑引入重大变化。然而,我们决定对此类特例保持沉默,而不冒着破坏客户集成的风险,以保持一致性。
在极少数情况下,我们做出了严重的决定,从而进行了重大变化 - 例如弃用某个 API 资源或方法 - 这是出于更大的用户社区的利益,且仅在确认对用户的影响几乎为零时才会进行。例如,我们故意选择改变 Suppression API 的响应行为,但都是经过仔细权衡收益和对社区的影响,并且仔细地向我们的用户传达这一变化。然而,我们绝对不会引入任何有可能直接影响用户生产电子邮件发送的更改。