撰写出色的REST API的艺术
在编写API时,REST(代表表现层状态转移)被认为是标准。然而,REST本身并不是一个标准。这使得设计直观的REST API变得棘手。它更像是一种思维方式或一种艺术形式,而不是一份清单。
创建出色的API有许多组成部分。许多开发者做对了一些事情,但也有很多做错了。那么,如何思考设计API?什么样的API才是好的?
一致性
首先,你正在创建一个别人需要学习如何使用的接口。如果你在整个接口中保持相同的范例,那么对于使用者来说会更容易一些。
一致性从大的方面来讲,比如如何划分资源,一直到小的方面来讲,比如如何命名事物。除此之外,与其常用的API也尽量要保持一致。
对于编写API的人来说,可以创建一个标准。单即使有一个表证非常全面,还会存在一些边界情况。对于所有这些边界情况,我们应该思考:
- 以前是如何做的?
- 其他API是如何做的?最常见的是什么?为什么有些公司偏离了这个(是设计不好,还是有充分的理由)?
- 针对未来可能性的案例,如何设计是合理的?
- 最直观和最不令人困惑的是什么?其他一些开发者认为什么是最直接的?
当第一次制定API标准时,至少有一些公共API。尽管如此,仍然应该思考,如果今天可以重新编写所有API,它们理想中会是什么样子?这个标准应该优先考虑一致性,并可以作为当前API和理想状态之间的桥梁。
下一步是确定如何对API进行版本管理或逐步调整,以将当前版本逐渐转变为理想版本。
一切都关乎资源
资源是REST规范中明确指定的内容之一。REST API围绕资源展开。REST的整个概念是将资源暴露给网络。
这些资源应该是完全稳定的。所谓稳定,是指返回特定资源的每个端点都应该返回相同的资源表示。不同的端点不应该返回不同的字段;该资源的概念应该始终保持一致。
唯一可能返回除完整资源之外的情况是关系——例如,一个包含对另一个资源的引用的资源(比如,它有一个父对象)。在这种情况下,可以有被引用资源的最小表示。这在多服务架构中尤其有用,因为API的服务可能不拥有所有其他被引用的资源,因此只需要负责返回自己的数据。这个最小表示应该包含拉取完整资源所需的信息(通常是两个字段:类型和ID)。
在设计这些资源时,请记住你的API资源不需要与数据库中的对象完全匹配。把它看作是重新设计数据的机会,使其更合理。显然,你也不希望在每个API调用中进行20个复杂的数据库查询。然而,在合理的情况下,你可以并且应该重命名事物并混淆混乱的概念。仅仅因为你有三个不同的数据库对象表示用户是否被邀请、确认或归档,并不意味着API的使用者需要知道这一点。
如果不同的资源上的操作似乎在尝试做不相关的事情,那么可能是你混淆了应该是多个资源的概念。虽然你不希望每个调用你的API的人在每次尝试做任何事情时都要进行十次调用,但你也不希望混淆概念。你保持模块化的程度越高,你的API对用户来说就越灵活,他们理解起来也越容易。
ID和类型推动世界运转
在你的API中使用唯一的ID。一直以来都是如此。我还没有找到一个唯一的ID不适用或没有意义的情况。我已经见过太多相反的情况了——没有ID,也没有好的方法添加ID。所有返回的响应中的资源都应该始终包含一个ID。所有的GET端点都应该有根据ID获取的选项。如果你希望允许通过名称或其他方式获取资源,请实现一个查询过滤器,但不应该跳过ID。此外,这个ID应该是唯一的。它不必在所有资源中唯一,但在给定资源类型的所有对象中应该是唯一的(过去、现在和未来)。如果我删除一个资源然后稍后尝试获取它,我永远不应该得到一个不同的资源。这个ID不一定是数据库ID——它可以使用几个字段并将它们组合或哈希在一起。事实上,无论ID是什么,只要它唯一地标识一个对象就可以。
同样地,你应该始终在资源中返回一个类型字段。这可能看起来多余——你调用了GET /flowers,所以很明显响应中会有”type”:”flower”。然而,返回类型是有好处的。对于可能返回多种类型的任何端点或字段,类型字段标识了每个资源的类型。此外,它使用户能够获取完整的对象(因为你知道要使用哪个端点)。举个例子,你可以在更改中有一个审批人字段。这些审批人可以是用户或者组。类型字段允许你区分它们。包含类型还为你提供了未来的灵活性。即使你现在不允许组审批,也并不意味着你将来不会添加它们。有了类型的存在,你可以在不产生破坏性变化的情况下进行更改。如果没有类型,任何针对你的API编写的代码都会假设之前的类型。如果无法再推断出该类型,任何具有该假设的代码都会失效。类型只在某些场景下有用,所以只在需要的情况下使用它可能是很诱人的。然而,反对意见回归到一致性和稳定性的资源——你的资源应该始终具有相同的字段。这也适用于类型。始终包含类型比在可以起到决定性作用的情况下没有类型要容易得多。
资源名称很重要
正如我之前所说,资源是你的API的核心。然而,如果没有人知道它们代表什么,它们几乎没有用处。名称应该准确清晰地表达一个资源代表的内容。此外,它们应该始终是复数名词。资源是对象,所以它们应该始终是名词,因为对象是名词,不是动词。
但为什么要用复数形式?我对此的理解是,资源端点不代表资源的单个实例。相反,它引用了服务器上该资源类型的所有实例。例如,端点/plants代表服务器上的所有植物。GET /plants应该返回这些植物的列表。POST /plants向这个植物池添加一个项目——我正在向植物中添加某个东西。即使在GET /plants/{plantId}的情况下,它预计只会返回一个项(因为有唯一的ID,还记得吗?),它也是将所有植物的列表缩小到一个特定的项。就像是在所有植物中,过滤出具有这个ID的那一个。因此,资源应该始终是复数名词。同样地,我可以用/plants?flower_color=blue,它应该返回所有具有蓝色花朵的植物的列表。
遵循相关的标准
虽然REST没有一个标准,但它使用了一些其他有标准的协议。例如,大多数人使用JSON和HTTP编写REST API。虽然这两者都不是必需的,但两者,特别是HTTP,几乎是普遍适用的。如果你使用JSON和HTTP,你应该遵守它们的标准。JSON标准很简单。而HTTP标准则意料之中地非常冗长。其中有很多内容。有几个特定的事情,我想要确保指出来。那就是正确使用状态码和正确使用HTTP方法。这两者都足够复杂,以至于我分别写了关于状态码和HTTP方法的博客。对于任何良好的API来说,正确处理这些是必不可少的。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接