Stripe的API实战设计模式 - Paul


在这里介绍了一些API设计模式,这些模式足够通用,对 API 设计过程中的几乎任何人都有用。

语言
给事物命名是很困难的。计算机科学中的大多数事情都是如此,API 设计也不例外。这里的问题是,与变量和函数名称类似,您希望 API 路由、字段和类型清晰而简洁。即使在最好的情况下,这也很难做到,但这里有一些技巧。

1、使用简单的语言
这是一个显而易见的建议,但实际上很难做到,尝试将概念提炼到其核心,并且不要害怕使用同义词库。
例如,在构建平台时,不要混淆用户和客户的概念。用户(至少在 Stripe 术语中)是直接使用您平台产品的一方,客户(也称为“最终用户”)是最终购买您的用户可能提供的商品或服务的一方。您不必使用这些确切的术语(“用户”和“最终用户”都可以),只要与您的语言保持一致即可。

2、避免使用行话
各行各业都有自己的行话;不要假设您的用户了解有关您的特定行业的所有信息。例如,您在信用卡上看到的 16 位数字称为主帐号,简称 PAN。在金融科技圈子里,人们谈论 PAN、DPAN 和 FPAN 是很正常的,所以你在支付 API 中做这样的事情是情有可原的:
card.pan = 4242424242424242;

即使您知道 PAN 代表什么,它与信用卡上的 16 位数字之间的联系可能仍然不明显。相反,避免使用行话并使用更容易被更多受众理解的内容:
card.number = 4242424242424242;

当考虑 API 的受众是谁时,这一点尤其重要。实现 API 的人员的核心角色很可能是不了解金融科技或任何其他专业领域的开发人员。

一般来说,最好假设人们不熟悉您所在行业的术语。

结构
更喜欢枚举而不是布尔值
假设我们有一个用于订阅模型的 API。作为 API 的一部分,我们希望用户能够确定相关订阅是否处于活动状态或是否已被取消。拥有以下接口似乎是合理的:
Subscription.canceled={true, false}

这是可行的,但是在实施上述一段时间后,我们决定推出一项新功能:暂停订阅。

暂停订阅意味着我们暂停付款,但订阅仍然有效且未取消。

为了反映这一点,我们可以考虑添加一个新字段:

Subscription.canceled={true, false}
Subscription.paused={true, false}

现在,为了查看订阅的实际状态,我们需要查看两个字段而不是一个。
这也给我们带来了更多困惑:如果订阅有取消:true 和暂停:true,该怎么办?已取消的订阅也能暂停吗?也许我们可以将其视为一个错误,并规定已取消的订阅必须有 paused: false。

现在,为了查看订阅的实际状态,我们需要查看两个字段而不是一个。

这也让我们陷入更多困惑:如果订阅有canceled: true和paused: true,该怎么办?已取消的订阅还可以暂停吗?

也许我们可以将其视为一个错误,并规定已取消的订阅必须有 paused: false。
这是否意味着取消的订阅可以重新暂停?

添加的字段越多,问题只会变得更糟。

您需要一堆令人困惑的 if/else 语句来准确弄清楚此订阅发生了什么,而不是能够检查单个值。
相反,让我们考虑以下模式:
Subscription.status={"active", "canceled"}

单个字段通过使用枚举而不是布尔值以简单的语言告诉我们对象的状态。

另一个好处是该技术为我们提供的可扩展性和面向未来的能力。如果我们回到之前添加“暂停”机制的示例,我们需要做的就是添加一个额外的枚举:
Subscription.status={"active", "canceled", "paused"}

我们添加了功能,但将 API 的复杂性保持在同一基线,同时也更具描述性。如果我们决定删除订阅暂停功能,删除枚举总是比删除字段更容易。

这并不意味着您永远不应该在 API 中使用布尔值,因为几乎可以肯定在某些边缘情况下它们更有意义。
相反,我敦促您在添加它们之前考虑布尔逻辑不再有意义的未来可能性(例如,有第三种选择)。

使用嵌套对象实现未来的可扩展性
延续上一个技巧:尝试按逻辑将字段分组在一起。下列:

customer.address = {
  line1: "Main Street 123",
  city:
"San Francisco",
  postal_code:
"12345"
};

比以下干净得多:

customer.address_line1 = "Main street 123";
customer.address_city =
"San Francisco";
customer.address_postal_code:
"12345";

第一个选项使以后添加其他字段变得更加容易(例如,country如果您决定将业务扩展到海外客户,则添加一个字段),并确保您的字段名称不会太长。保持资源的顶层整洁不仅是更好的选择,而且还能抚慰心灵。

返回对象类型
在大多数情况下,当您进行 API 调用时,是为了获取或更改某些数据。

在后一种情况下,规范是返回变异资源的表示。例如,如果您更新客户的电子邮件地址,作为 200 响应的一部分,您会期望获得该客户的一份副本,其中包含新的、更新的电子邮件地址。

为了让开发人员的生活更轻松,请明确返回的内容。

在 Stripe API 中,我们object在响应中有一个字段,可以非常清楚地表明我们正在处理的内容。
例如,API 路由

/v1/customers/:customer/payment_methods/:payment_method

返回附加到特定客户的 PaymentMethod。

希望从路由中可以明显看出您应该期待 PaymentMethod 返回,但为了以防万一,我们包含该object字段以确保不会造成混淆:

{
  "id": "pm_123",
 
"object": "payment_method",
 
"created": 1672217299,
 
"customer": "cus_123",
 
"livemode": false,
  ...
}


这在筛选日志或在集成中添加一些防御性编程时非常有帮助:

if (response.data.object !== 'payment_method') {
  // Not the expected object, bail
  return;
}


使用权限系统
假设您正在为产品仪表板开发一项新功能,这是一位大客户特别要求的。您已准备好让他们将其作为测试版进行测试以获得一些反馈,因此您可以让他们知道向哪个路由发出请求以及如何使用它。新路线没有在任何地方公开记录,除了您的客户之外没有人应该知道它,所以您不必太担心。

几周后,您对功能进行了更改,以解决大客户给您的一些反馈,结果却收到其他用户发来的一系列愤怒的电子邮件,询问为什么他们的集成突然中断。

灾难,原来你的秘密 API 路由已经泄露了。

也许最初的客户对新功能非常兴奋,以至于他们决定将其告诉他们的开发人员朋友。或者,也许该客户的用户查看了他们的网络面板,看到了对未记录的 API 的这些请求,并决定他们喜欢自己产品的该功能的外观。

您不仅必须清理当前的混乱局面,而且现在您的测试版功能实际上已被拖入启动状态。由于现在进行任何新的更改都需要您通知您拥有的每个用户,因此您的开发速度已经减慢了。

对 API 进行逆向工程并不像您想象的那么困难,除非您采取措施阻止它,否则您可以假设人们会这样做。

隐匿性安全是指隐藏的东西因此是安全的。正如隐藏在衣柜里的圣诞礼物不是这样一样,网络安全也不是这样。如果您想确保您的私有 API 保持私有,请确保除非用户拥有正确的权限,否则它们无法被访问。

最简单的方法是将权限系统与 API 密钥绑定在一起。如果 API 密钥无权使用该路由,请提前保释并返回状态为 403 的错误消息。

让您的 ID 无法被猜测
如果您正在设计一个返回具有与其关联的 ID 的对象的 API,请确保这些 ID 无法被猜测或以其他方式进行逆向工程。如果您的 ID 只是连续的,那么您充其量只是无意中泄露了您可能不希望人们知道的有关您的业务的信息,最坏的情况是造成了潜在的安全事件。

举例来说,如果我在您的网站上进行购买并且收到确认订单 ID 为“10”,那么我可以做出两个假设:

  1. 你的生意并不像你声称的那么多
  2. 我也许能够获得有关之前 9 个订单(以及所有未来订单)的信息,但我不应该获得这些信息,因为我知道他们的 ID

对于第二个假设,我可以尝试通过以您不希望的方式滥用您的 API 来了解有关您其他客户的更多信息:
// 如果下面的路由没有权限系统、
// 我就可以猜到用户名,并获取潜在的私有
// 其他客户的信息

curl https:
//api.example.com/v1/orders/9 

// Response
{
   
"id": "9",
   
"object": "order",
   
"name": "Lady Jessica",
   
"email": "jessica@benegesserit.com",
   
"address": "1 Palace Street, Caladan"
}


相反,可以通过使用UUID 等方式使您的 ID 无法被猜测。使用本质上是一串随机数字和字母作为 ID 意味着无法根据您拥有的 ID 猜测下一个 ID 是什么样子。

您在便利性方面的损失(谈论“订单 42”比谈论“订单 123e4567-e89b-12d3-a456-426614174000”要容易得多),您将通过安全优势来弥补。不过,不要忘记通过添加对象前缀来使其易于阅读,以 order_3LKQhvGUcADgqoEM3bh6pslE 格式生成订单 ID 将使您和使用 API 构建的人员的生活变得更轻松。

为人类设计 API
关于如何设计 API 的资源有很多,我希望本文能给您带来一些启发,并激励您更深入地研究这个兔子洞。

在 Stripe,我们非常重视 API 设计。在内部,我们有一个设计模式文档,其中包含我上面所写的内容以及更多内容。它包括好的和坏的设计示例、值得注意的例外,甚至还提供了如何向现有资源添加枚举等内容的指南。

我最喜欢的部分是“不鼓励”部分,其中突出显示了当今 API 中存在的可疑设计示例,以作为对未来 Stripe 开发人员的警告。