写在前面
本文为转载的译文,原文链接为The Art of Writing Amazing REST APIs | by Joy Ebertz | Medium,原作者Joy Ebertz – Medium。
正文
当书写api时,REST(representational state transfer的缩写)被认作是标准。与此同时的是,REST其本身实际上也不是一种通用的标准。这使得我们难以设计直观的REST api。有一种方法,是将其视为一个表格而非清单。在经历两家不同公司的api标准形成过程之后,我可以告诉你,创造一个好的api需要考虑很多因素。其中一部分,我的公司做得很好;但更多的一部分,它做得相当糟糕。所以我们应该如何设计api?什么因素能够创造好的api?
一致性!
最初,也是最重要的:你在创造一个需要供其他人学习如何使用的接口。如果你在接口间维持相同范式,那么学习它会变得尤为容易。一致性存在于大工作,例如你如何分配资源,一致性也存在于小工作,例如你如何命名变量。除此之外,还有一件有用的事,api规范应该尽量和其他开发者的规范保持一致。如果开发者们已经熟悉了一套api,那么他们在上手相似的api时会非常迅速。对一致性的渴求表示我们可以创造出一份清单或是标准,这对我们所有的api设计者来说都非常幸运。不幸的是,即使是在最严格的标准下,边缘情况或是意料之外的情况依然存在。面对这类情况,我的应对如下:
- 我们之前做了什么
- 如果我们从未这样做过或者不一致,其他api会做什么?最常见的是什么?为什么有些公司会不这样做(这是一个糟糕的设计,或者他们有一个很好的理由)?
- 未来可能有哪些用例需要做类似的事情?有什么对他们来说是有意义的呢?
- 我如何写是最直观和最不令人困惑的呢?其他一些开发者认为最直观的是什么形式?
当您第一次为API设置一个标准时,几乎可以保证您已经有至少一些公共API。尽管如此,您仍然应该为您的理想案例编写标准。如果您今天可以重写所有的api,它们会是什么?在为理想情况编写此标准之后,您将需要一个当前API的标准。该标准应该优先考虑一致性,并可以作为当前api和理想状态之间的桥梁。下一步是弄清楚您想要如何使用这两种版本或缓慢地转换api,以将您的当前版本转换到理想版本。
都是关于资源的
资源是REST所指定的少数几件东西之一。RESTapi主要围绕着资源展开。REST背后的整个概念是,你是在向网络暴露资源。您还提供了对一组资源的操作。这些资源在您的API中应该是完全稳定的。我所说的稳定,是指返回特定资源的每个端点都应该返回该资源的相同表示。不同的端点不应返回不同的字段;该资源的概念应该始终相同。作为API的用户,我不应该猜测我将从端点返回哪些字段。
您可能返回完整资源以外的东西的唯一情况是关系——例如,包含对另一个资源的引用的资源(例如,它有一个父对象)。在这种情况下,对所引用资源的最小表示是可以接受的。这在多服务体系结构中尤其可取,因为API的服务可能不拥有被引用的所有其他资源,因此应该只负责返回它自己的数据。这个最小的表示应该包含提取完整资源所需的信息(比较典型的两个字段: type和id)。
在设计这些资源时,请记住,您的API资源不需要匹配数据库中的对象。你可以把它看作是你以一种有意义的方式重新设计数据的机会。显然,您也不想将自己置于必须为每个API调用执行20个复杂的数据库查询的位置上。然而,在它有意义的地方,您可以,也应该,重命名事物,并混淆混乱的概念。仅仅因为您有三个不同的数据库对象来表示用户是否被邀请、确认或存档,并不意味着API用户需要了解它。
如果某个特定资源上的不同操作似乎试图做不相关的事情,那么可能是你混淆了本应是多个资源的事务。虽然您不希望每个调用您的API的人在每次尝试做任何事情时都进行10次调用,但您也不想混淆概念。您保留的东西越模块化,您的API对用户就越灵活,他们也就越容易理解。
ID和type让全世界携起手来
在你的api里使用唯一的id。永远。我还从未发现唯一id不起作用的情况。但我见过太多相反的事情:没有id而且没有很好的添加id的方法。所有资源的恢复都应该包含一个id。所有的GET端点都应该有一个按ID进行GET操作的选项。如果您想允许通过名称或其他一些东西来获取资源,请实现一个查询过滤器,但您不应该跳过该ID。此外,此ID应该是唯一的。它不必在所有资源上都是唯一的,但它应该在给定资源类型(过去、现在和未来)的所有对象上都是唯一的。如果我删除了一个资源,并尝试稍后获取它,我应该永远不会得到一个不同的资源。这个ID不必是数据库ID——它可以使用几个字段,并将它们组合或散列在一起。事实上,只要ID是什么,它就可以唯一地标识一个对象。
类似地,您应该始终返回一个包含资源的类型字段。这可能看起来很多余——你叫GET/花,所以很明显,它的反应应该是“类型”:“花”。但是,我们有很好的理由返回该类型。对于可以返回多个类型的任何端点或字段,类型字段将标识每个这些资源的类型。此外,它还允许用户获取完整的对象(因为您知道要使用什么端点)。例如,您可能有一个关于更改的核准者字段。这些核准者可以是用户或组。该类型允许您区分它们。包括类型也会给你未来的灵活性。仅仅因为您现在不允许组批准,并不意味着您将来不会添加它们。拥有当前的类型可以让您改变它,而不会成为一个破坏性的改变。如果没有它,任何针对API编写代码的人都会假设一个类型。如果不能再推断出这种类型,那么任何具有该假设的代码都将中断。在这种情况下,类型只在某些场景下才有用,因此,它可能很容易只在需要它的情况下使用它。相反的参数可以追溯到一致性和稳定的资源——您的资源应该始终具有相同的字段。这也可以扩展到类型。总是包含类型比没有它更容易,因为它可以做所有的不同。
资源名称很重要
正如我之前所说的,资源是你的api的灵魂。然而,如果没有人知道它们代表什么,它们几乎是无用的。名称应该准确而清楚地表达资源所代表的内容。此外,它们应该总是复数名词。资源是对象,所以它们应该总是名词,因为对象是名词,而不是动词。
但为什么是复数?我的想法是,资源端点并不代表资源的单个实例。相反,它引用了服务器上所有该资源类型的池。例如,端点/植物表示服务器上的所有植物。获取/plants应该返回一个这些植物的列表。POST/plants向植物池中添加一个项目——我在向植物添加一些东西。即使是在GET/plants/{plantId}的情况下,它预计将只返回一个项目(因为唯一的ID,还记得吗?),它正在把所有植物的名单缩小到一个特定的名单。这就像是说,在所有这些植物中,过滤到有这个ID的那个。因此,资源应该始终是复数名词。同样,我可以做/plants?flower_color=blue,它应该返回所有有蓝色花的植物的列表。
遵循相似标准
虽然REST没有一个标准,但它确实使用了其他一些确实有标准的协议。例如,大多数人使用JSON和HTTP来编写RESTapi。这些都不是必需的,但两者,特别是HTTP,几乎都是通用的。如果您使用JSON和HTTP,那么您应该遵守它们的标准。JSON的标准很简单。与此同时,HTTP标准无疑也相当冗长。这里面有很多东西。有一些事情,我想确保特别指出。这些都是正确使用状态代码的方法和正确使用HTTP方法。它们都足够复杂了,所以我已经为状态代码和HTTP方法编写了单独的博客。正确的API都是必不可少的。
完整性
如果您已经实现了HATEAOS (Hypermedia as the Engine of Application State 超媒体作为应用程序状态的引擎)链接,人们通常会认为它是下一个级别的REST或真正的RESTful。也就是说,HATEAOS链接虽然在概念上很酷,但对大多数人使用现代api并没有用处。因此,在几乎所有的情况下,我都建议不要实施它们。也就是说,我仍然喜欢HATEAOS背后的想法。简短地说,HATEAOS的主要思想是创建一种遍历API的方法,除了一个起点之外,没有任何了解该API的先验知识。每个响应都包含到相关资源的链接,等等。我不认为HATEAOS链接是有用的,因为几乎任何针对你的API构建的明智的人都会先查看你的文档。他们还在编写稳定的代码,它依赖于特定的、已知的端点,而不是随机地遍历事物。
然而,如果您可能不会构建HATEAOS链接,但您应该确保用户可以从一个资源遍历到另一个资源。他们应该能够单独通过API来完成工作流。这意味着,如果我在文件资源中引用了一个父文件夹,那么我还应该有一个GET端点来获取该父文件夹。这再次重申了id的重要性。如果我列出与用户相关联的组,我不应该只列出组名称;我应该包括ID,以便API用户可以获得关于或操作这些组的更多信息。他们应该能够从一个资源浏览数据到另一个资源。
大多数api包含一些漏洞,并且与它们的UI产品缺乏一些相同的地方,但尽可能限制漏洞是理想的。添加api可能会很耗时,所以在做出权衡时,我会考虑让所有api对给定的资源可用,而不是在一堆资源中添加一些api。
api的部分美妙之处在于,它们打开了原公司从未梦想过的功能可能性。它们允许开发人员将功能扩展到UI中提供的用例之外,并创建全新的流。考虑到这一点,不要只是为您所知道的用例填充api,而是尝试从核心对象开始填充尽可能多的api。
最后一点
由于每个人都在谈论REST,但没有实际的标准,因此编写好的、可用的和可扩展的api可能具有挑战性。设计需要很容易的时间和实践,但也有一些重要的事情需要记住。你的api可以通过花时间仔细思考资源来改进——总是使用复数名词,确保包括类型和id,并遵循相关的标准。您可以通过尽可能多地为我们的产品提供API,进一步使开发人员能够使用您的API。最后,最重要的是,您的API和与其他API之间的一致性将使您的API易于使用。