那么,给一个API版本化有多难呢?真相是这并不难,但困难在于保持某种理智,而不是不必要地陷入数量繁多、相互不兼容的版本和子版本中,这些版本和子版本应用于数十个API端点。
Business in a box.
探索我们的解决方案。
与我们的销售团队交谈
重大更改糟糕!API版本管理好!
任何构建或经常使用API的人迟早会意识到,重大变更是非常糟糕的,并且可能对一个原本有用的API造成非常严重的伤害。重大变更是指API行为的改变,它可能会破坏用户的集成,导致大量的挫折以及API提供者和用户之间信任的丧失。重大变更要求用户提前收到通知(以及随附的道歉),而不是像令人愉悦的新功能那样突然出现。避免这种挫折的方法是对API进行版本控制,并从API所有者那里获得保证,在任何单个版本中不会引入令人惊讶的更改。
那么,对API进行版本控制会有多难呢?事实上并不难,但难的是保持一些理智,不要无谓地演变到几十个API端点上应用头晕目眩的多个版本和子版本,并且兼容性不明确。
我们在三年前引入了API的v1版本,并没有意识到它会一直走到今天。那么我们如何在两年多的时间里持续提供最佳的电子邮件传递API,但仍维持同一个API版本呢?虽然对如何对REST API进行版本控制有很多不同的意见,但我希望我们这个不起眼却强大的v1版本的故事,能在你通往API版本控制启迪的道路上为你指引方向。
REST Is Best
SparkPost API 源自我们还是 Message Systems 的时候,在我们涉足云之前。那时,我们正在为 Momentum 4 的测试版发布做最后的准备。这是 3.x 版本的重大升级,我们市场领先的本地 MTA。Momentum 4 包含全新的 UI、实时分析,最重要的是一个用于消息注入和生成、管理模板和获取电子邮件指标的新 Web API。我们的愿景是实现 API 优先的架构——即使 UI 也会与 API 端点交互。
我们做出的最早和最好的决策之一是采用 RESTful 风格。自 2000 年代后期以来,表示状态传递(REST)基础的 Web API 是云 API 的事实标准。使用 HTTP 和 JSON 可以让开发人员更轻松地集成我们的 API,无论他们使用哪种编程语言——PHP、Ruby 和 Java ——而不必了解或关心我们底层的技术。
选择使用 RESTful 架构很容易。选择版本控制约定就不那么容易了。最初,我们通过完全不对测试版进行版本控制来回避这个问题。然而,不到几个月,测试版到了少数客户手中,我们开始构建我们的云服务。是时候进行版本控制了。我们评估了两种版本控制方案。第一个是将版本直接放在 URI 中,第二种是使用 Accept 头。第一个选项更明确,也更简单,这对开发人员来说更容易。由于我们爱开发人员,这是合乎逻辑的选择。
API 管理
选择了版本控制惯例后,我们遇到了更多问题。我们什么时候会提升版本?什么是重大变更?我们会重新版本化整个API还是仅某些端点?在SparkPost,我们有多个团队在处理API的不同部分。在这些团队中,人们在不同的时间处理不同的端点。因此,我们的API在使用惯例时保持一致性非常重要。这比版本控制更重要。
我们成立了一个治理小组,包括代表每个团队的工程师、产品管理团队的一名成员和我们的CTO。该小组负责在所有团队中建立、记录并执行我们的API惯例。一个API治理Slack频道在关于该主题的热烈讨论中也派上了用场。
治理小组确定了若干可以引入API的方式,这些更改对用户有利且不构成重大变更。这些包括:
新资源或API端点
新的可选参数
非公开API端点的更改
JSON POST主体中的新可选键
JSON响应体中返回的新键
相反,重大变更包括任何可能损坏用户集成的内容,例如:
新的必需参数
POST主体中的新的必需键
删除现有端点
删除现有端点请求方法
API调用的内部行为实质性不同,例如更改默认行为。
The Big 1.0
在我们记录和讨论这些约定时,我们也得出结论,为了大家(包括我们自己!)的最佳利益,应该避免对API进行重大更改,因为管理多个版本会增加不少工作量。我们决定在承诺“v1”之前,有一些事情需要在我们的API中修复。
发送一封简单的电子邮件所需的努力太大了。为了“保持简单的事情简单”,我们更新了POST正文以确保简单和复杂的用例都得到满足。新的格式在未来也更加可靠。其次,我们解决了Metrics端点的问题。此端点使用了一个“group_by”参数,该参数会更改GET响应正文的格式,使得第一个键将是group_by参数的值。这看起来并不符合REST的风格,所以我们将每个group_by分成了独立的端点。最后,我们审核了每个端点,并在各处做了一些小的修改以确保它们符合标准。
准确的Documentation
拥有准确且可用的API文档对于避免故意或无意的重大更改非常重要。我们决定使用一种简单的API文档方法,利用一种称为API Blueprint的Markdown语言,并在Github上管理我们的文档。我们的社区对这些开源文档进行贡献和改进。我们还在Github中维护了一组非公开的文档,用于内部API和端点。
最初,我们将文档发布到Apiary,这是一个用于原型设计和发布API文档的出色工具。然而,将Apiary嵌入我们的网站在移动设备上不起作用,因此我们现在使用Jekyll来生成静态文档。我们的最新SparkPost API文档现在加载迅速,并且在移动设备上表现良好,这对不总是坐在计算机前的开发人员来说很重要。
分离 Deployment 和 Release
我们很早就学到了一个宝贵的技巧,即将部署与发布分开。这样一来,当变更准备好后,可以通过持续交付和部署频繁地进行部署,但我们不总是同时公开宣布或记录这些变更。对于我们来说,在UI中或使用内部工具时,先部署一个新的API端点或对现有API端点的增强,而在公开记录和支持之前使用它,这并不罕见。这样,我们可以对其进行一些调整以提高可用性或符合标准,而不必担心造成令人害怕的重大变更。一旦我们对更改感到满意,我们就会将其添加到我们的公共文档中。
哎呀!
承认我们有时未能坚持“无破坏性更改”理念是公平的,这些都是值得学习的经验。曾有一次,我们认为如果某个属性默认设置为true而不是false,对用户会更好。当我们部署更改后,收到用户的多次投诉,因为行为发生了意外的变化。 因此我们撤销了更改,并添加了一个账户级别的设置——这显然是更用户友好的方法。
有时我们很想因为修复错误而引入破坏性更改。然而,我们决定保留这些特例,而不是为了保持一致性而冒着破坏客户集成的风险。
也有少数情况下,我们做出了重大的决策来进行破坏性更改——例如弃用一个API资源或方法——以便于更广大用户群体的利益,并且仅在确认对用户没有或几乎没有影响之后。例如,我们故意选择更改了Suppression API的响应行为,但仅是在仔细权衡了对社区的好处和影响并与用户充分沟通之后。然而,我们绝不会引入任何有可能直接影响用户生产邮件发送的更改。