REST
API 通过 HTTP 运行,使用
GET
和
POST
等标准动词,公开常识性 URL 结构并以明确定义的格式(通常是 JSON)返回资源。
尽管定义很简单,但在设计一个 REST API 来搞砸它时,仍然有很大的自由度。不要那样做!遵循使调用开发人员尽可能简单的指导原则。提供正确的粒度级别,减少他们需要拨打的电话数量,并记录下来。
就粒度而言,我指的是 API 是否直接公开单个数据库记录,使用薄的 CRUD 层,而不是将它们组合成更高级别的资源,以用户认为的方式代表问题域。 CRUD 模型对于内部低级数据 API 很有意义,而更高级别的模型对于暴露给外部世界以及内部前端和移动开发人员更有意义。请记住——您的关系数据模型细节几乎永远不会与用户对您的应用程序如何工作的心智模型保持一致。
对于本文的其余部分,我将主要讨论更高级别的 API,在此称为 Intent API 。
资源语义
Intent API 和 CRUD API 有什么区别?粒度级别。我们没有公开您的实际数据库方案(可能会更改)的实现细节,而是公开了用户对您的实际用例的意图的更高阶概念。
例如,在银行的 CRUD API 中,您可能会公开帐户、帐户持有人和交易的 资源 。您将允许呼叫者创建 Accout 记录,并且您将允许创建 Transactions。调用者可能将两个账户之间的转账作为两个交易来实现;一笔从账户 A 借记,一笔存入账户 B。希望您也可以以某种方式使这两个操作成为原子交易。
使用 Intent API,您可能根本不允许创建帐户或帐户持有人。这些可能是您希望由真人执行的离线手动任务,或者至少是您自己的内部服务,然后使用私有 CRUD API。您也可能不想直接创建交易。但也许您公开了一个转移资源,以及购买和退款资源。
这对你有什么好处?通过映射到用户的意图——他们实际想要完成的事情——你有机会定制一个 API 端点,使其仅适合对该操作有意义的参数集。对于转账,您需要两个账户 ID。对于购买,您需要供应商元数据。对于 ChargeBack,您需要一个以前的交易 ID。
您还可以确保操作是原子的,并使数据处于有效状态。如果 Transfer 在两阶段提交的第二部分失败,您可以回滚第一部分。您不必依赖调用者来正确执行此操作。
您所做的是从调用者那里消除实施特定于您的系统的业务逻辑的负担,并将其放置在您的系统中(它所属的位置)。
这确实不是严格意义上的 RESTful;您将动词作为您的资源公开。您可能还会公开名词(帐户、交易、供应商),但您的任何非 幂 等调用都应该是动词。
有关此操作的示例,请参阅 GitHub API :
POST /repos/:owner/:repo/merges
{
"base": "master",
"head": "cool_feature",
"commit_message": "Shipped cool_feature!"
}
请注意,从语义上讲,您并不是在创建一个真正代表 git 中两个分支合并的提交资源。大多数 git 用户甚至不了解合并的数据模型实际发生了什么;也不要让您的 API 调用者必须理解它。
您如何知道要为哪些意图建模?我将从查看您的核心用户场景开始,并考虑您希望每个场景拥有哪些资源,同时记住您希望最大限度地减少显示用户界面所需的 API 调用次数。您必须平衡它与每个资源的关注点的凝聚力和分离度。
网址结构
关于实施细节!您的 URL 结构应该是什么样的?通常,您要选择复数或单数名词和动词并坚持使用。我会自以为是地说你应该使用复数名词和单数动词。例如:
-
/accounts
- 列出所有账户 -
/accounts/123
- 获取 ID 为 123 的帐户 -
/accounts/123/transactions
- 列出与此帐户关联的交易 -
/accounts/123/transactions/123
- 获取账户内的特定交易 -
/transactions/123
- 在账户上下文之外获取相同的交易 -
/transfer
- 创建两个账户之间的转账 (POST)
注意: 在多个端点暴露同一个 Resource 是没有问题的。这不是 DRY 模型;请记住,我们的指导原则是让开发人员的工作变得简单。也许他们不知道哪个帐户与给定的交易相关联。
最后,您想考虑在
/
的根端点显示什么内容。我已经看到一些 API 使用该端点作为机会来包含喜欢开发人员文档和/或系统中所有端点的列表。
要求
这里最大的决定是如何从调用者那里获取数据。大多数 REST API 将支持大多数用例的 URL 参数。如果您这样做,请确保也支持表单 POST 编码,这应该不是额外的工作。这些适用于简单的键/值参数,并且很容易为调用者实现。
对于嵌套数据,您可以选择支持 JSON 请求主体或在键/值对中使用某种类型的前缀方案。例如,您可以表示以下 JSON 正文:
POST /repos/:owner/:repo/merges
{
"base": "master",
"head": "cool_feature",
"commit_message": "Shipped cool_feature!"
}
作为键/值对
?user=Chase Seibert&account__id=1&account__name=foobar
。就我个人而言,我认为在某些情况下,这对于调用者来说既难看又难以实现。
无论您选择什么,请务必检查并尊重调用者的内容类型。
元数据和响应
对于每个 API 响应,您希望拥有一组一致的元数据,调用者可以依赖这些元数据,以及一致的整体数据包结构。例如,您可能希望为分页、结果和错误消息定义明确的字段。但您可能还想包括不太明显的项目,例如用户传递给您的参数的回显。这可以用作您已明确接收到参数、正确解析它们并且它们对于此 API 调用有效的信号。
分页通常通过支持
limit
、
offset
和
sortBy
之类的东西作为 URL 参数来完成。然后,您在响应中包含
nextPage
和
previousPage
字段
,它们是 API 中这些结果的绝对 URL
。
注意:
我在这里使用的是
camelCase
而不是
snake_case
。鉴于如今大多数 API 使用者要么是 Javascript 应用程序要么是本机移动应用程序(objective-c 或 Java),使用它们的约定并使用驼峰式大小写可能是有意义的。只要保持一致。
错误消息对开发人员的理智很有帮助。当然,您希望错误的主要信号是该错误类的正确 HTTP 状态代码。
版本控制
您可能需要预先考虑 API 版本控制策略。在最简单的形式中,这只是一个像
/v1
这样的前缀,您可以将其添加到每个 API 端点。这个想法是提前计划同时运行多个受支持的版本,让开发人员能够温和地过渡到对 API 的重大更改。
但是,您如何构建您的代码才能为多个版本提供服务?笨手笨脚的方法是为每个支持的版本分叉代码。这相当简单,并且具有对于旧版本非常可预测的稳定性的优势。一种不同的方法可能是让相同的代码库服务于多个版本。在这种情况下,您可能还希望保留单元测试子集的多个版本。
第三种混合方法是使用源代码控制分支或实际运行的 VM 或带有该代码的容器进行分叉。这样做的缺点是使旧版本难以修补,无论是修补程序还是基础架构更改。
最重要的是提前做好计划。我建议同时使用
/v1
和
/v0
API 启动,它们具有一些向后不兼容的差异,即使它只是在版本 1 中删除的虚拟端点。
验证
如果您正在制作一个面向公众的 API,您几乎肯定想要使用 OAuth。不要自己写,找一个框架。即使您的 API 仅限于内部使用,您也应该考虑至少包含某种调用方标识符机制。当您要对谁在使用 API 进行分析时,这会派上用场,此外这也是对每个调用者进行速率限制的先决条件。
无论您选择哪种身份验证机制,您都希望 100% 的 API 调用通过 HTTPS,以免泄露这些凭据。甚至不支持 HTTP 选项。
文档和开发人员
几乎与 API 的语义一样重要的是拥有优秀、全面的文档。不要依赖这里自动生成的文档。或者,至少添加大量解释性细节,说明开发人员可能想要使用 API 的原因,以及请求和响应的每一部分的含义。将自己置于一个不知道您系统中模型的私密细节的人的位置上尤其棘手。由第三方运行它以进行健全性检查。
除了文本文档之外,您还需要为常见的请求和响应提供完整的、未截断的示例。继续并确保它们打印漂亮,甚至语法突出显示。我还建议您漂亮地打印来自服务器的实际 API 响应。
公开示例的一种好方法是使用交互式控制台。如果您提供 HTML 文档,您甚至可以使示例可执行并可在线调整。 Django REST 框架 就是一个很好的例子。
Python工具
以下是一些用 Python 编写 REST API 的常用框架:
Bunch 是使用 REST API 的一个很好的实用程序,它使您可以轻松地在 JSON API 响应和 Python 对象之间进行转换。您也可以采用其他方式,这对于将数据库对象映射到 JSON 可能很有用。
对于版本控制,请查看 Flask 蓝图 。
最后,根据您的 API 是内部的还是外部的,您可以查看用于创建 API 控制台的工具: