Strip 的滚动 API 设计经验
APIs as infrastructure: future-proofing Stripe with versioning
如果某个 API 返回一个名为 verified
的 boolean
类型字段来表示账户状态,那么用户可能会编写出如下的代码
if bank_account[:verified]
...
else
...
end
但假如在后续迭代中将 verified
替换为包含 verified
的 status
字段,那么便会影响用户之前所写的代码。这种类型的改变称为 "backwards-incompatible"。为了避免这种情况,之前存在的字段应该继续存在且保持相同的类型和名称。显然这对开发并不友好,但如果要用户更新他们的代码则又需要经过足够的协商
通常的方法是使用版本控制,v1
、v2
、v3
作为前缀添加在 URL 中或者 HTTP ACCEPT
首部中。OK 还行,但如果版本间变化太大会导致用户不愿意升级继续使用旧版本的 API。Stripe 使用滚动版本来实现版本控制,这些版本以发布日期来命名,比如 2017-05-24
,携带在请求头 Stripe-Version
中。它们虽然也是 "backwards-incompatible" 的,但每个版本仅包含一小组改动,使得升级变得容易。这种方式有点类似于敏捷开发
但版本控制始终是改善开发人员体验和维护旧版本的额外负担之间的折衷。Stripe API 的每个响应都由 APIResource 类进行描述
class ChargeAPIResource
required :id, String
required :amount, Integer
end
当需要进行 "backwards-incompatible" 的更改时,将其封装在 version change module 中。此模块定义了关于此次更改的文档和转换以及修改的 API 资源类型
class CollapseEventRequest < AbstractVersionChange
description \
"Event objects (and webhooks) will now render " \
"`request` subobject that contains a request ID " \
"and idempotency key instead of just a string " \
"request ID."
response EventAPIResource do
change :request, type_old: String, type_new: Hash
run do |data|
data.merge(:request => data[:request][:id]) # rollback
end
end
end
将这些 version change module 与 API 版本进行关联
class VersionChanges
VERSIONS = {
'2017-05-25' => [
Change::AccountTypes,
Change::CollapseEventRequest,
Change::EventAccountToUserID
],
'2017-04-06' => [Change::LegacyTransfers],
'2017-02-14' => [
Change::AutoexpandChargeDispute,
Change::AutoexpandChargeRule
],
'2017-01-27' => [Change::SourcedTransfersOnBts],
...
}
end
当生成响应的时候,API 根据当前版本的 APIResource 来格式化数据,然后根据 Stripe-Version
来决定最终的版本,回溯并应用沿途找到 version change module,得到目标版本的 API 响应
这种方案的另一个优点是易于生成 changelog