怎样编写好的API?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文最初发表于"},{"type":"link","attrs":{"href":"https:\/\/www.stxnext.com\/blog\/how-to-build-a-good-api-that-wont-embarrass-you","title":"","type":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"STX Next博客网站"}]},{"type":"text","marks":[{"type":"strong"}],"text":",经原作者Sebastian Buczyński同意由InfoQ中文站翻译分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在,每个人都在关注API。API最早开始流行于大约20年前,2000年,Roy Fielding在他的博士论文中首次提出了REST这个术语。同年,Amazon、Salesforce和eBay向全世界的开发者介绍了他们的API,永远改变了我们构建软件的方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在REST之前,Roy Fielding论文中的原则被称为“HTTP对象模型”,随后你会明白这为何非常重要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"随着阅读的深入,你还会看到如何确定你的API是否成熟,好API的主要品质是什么以及为何在构建API的时候,要注重适应性。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"RESTful架构基础"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST代表表述性状态转移(Representational State Transfer),由Roy Fielding在他的博士论文中定义,长期以来,它就是服务API的圣杯。它并不是构建API的唯一方式,但是由于它的流行,即便是非开发人员也知道这种标准。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RESTful软件有如下六种特点:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"客户端-服务器端架构"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"无状态"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"可缓存"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"分层系统"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"按需编码(可选)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"统一接口"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,对日常使用来说,这过于理论化了。我们需要更具操作性的东西,这也就是API成熟度模型。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Richardson成熟度模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"该模型是由Leonard Richardson提出的,它将RESTful开发原则结合成四个简单易行的步骤。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/75\/753f4add733015c0fb3752a01bc2002a.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在模型中的位置越高,就越接近Roy Fielding所定义的RESTful原始理念。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 0:POX(Plain Old XML)的泥沼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Level 0的API是一组简单XML或JSON的描述。在前文中,我曾经说过在Fielding的论文之前,RESTful原则被称为“HTTP对象模型”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这是因为HTTP是RESTful开发中最重要的组成部分。REST要尽可能多地使用HTTP固有属性中的理念。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Level 0,没有使用任何这样的东西。我们只是构建自己的协议并把它作为一个专有层。这种架构被称为远程过程调用(Remote Procedure Call,RPC),适用于远程过程\/命令。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我们会有一个端点,可以对它进行调用以获取一堆XML。在这方面,一个典型的例子就是SOAP协议:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/97\/976134016402f7d1654992feb0d16e60.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一个很好的例子就是Slack API。它有些多样化,有多个端点,但依然是RPC风格的API。它暴露了Slack的各种功能,中间没有附加任何特性。如下的代码展示了如何向一个特定的通道发送消息:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/51\/517deffff1b1e1b957ca6d32690c9e87.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虽然按照Richardson的模型,这是一个Level 0的API,但是这并不意味着它是不好的。只要它是可用的,并且恰当地服务于业务需求,那它就是很棒的API。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 1:资源"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了构建Level 1的API,我们需要找出系统中的名词并将它们通过不同的URL暴露出来,如下面的样例所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/68\/686b5ce2484955677e65b7bc67d6dae3.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中,“\/api\/books”能让我访问一个通用的图书目录,“\/api\/profile”能够让我访问这些书的作者的基本信息。为了获取某个资源的第一个特定实例,我可以在URL中添加ID(或其他引用)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在URL中还可以嵌套资源,这展示了它们是以层级结构的形式组织的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回到Slack的样例,如下展示了按照Level 1 API,它们会是什么样子的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5f\/5fa57bc77520b436c402e2139379c57b.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在,URL发生了变化,从原先的“\/api\/chat.postMessage”变成了现在的“\/api\/channels\/general\/messages”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"信息中“channel”部分从请求体转移到了URL中。从字面就能看出,通过使用这个URL,我们可以预期有条消息发布到了“"},{"type":"text","marks":[{"type":"italic"}],"text":"general"},{"type":"text","text":"”通道上。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 2:HTTP动作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Level 2利用HTTP动作(verb)来添加更多的含义和意图。在这方面可用的动作比较多,我这里只用到一个基础的子集:PUT \/ DELETE \/ GET \/ POST。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"借助这些动作,我们可以预期包含它们的URL有不同的行为:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"POST:创建新数据"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PUT:更新现有的数据"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DELETE:移除数据"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GET:查找特定id的数据输出,获取某个资源(或整个集合)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上面提到的“\/api\/books”为例:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ec\/ec06b380deed723197283a85e16535f0.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那这里的“安全”和“幂等”又是什么意思呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“安全”的方法指的是永远不会改变数据的方法。REST建议GET方法只能用来获取数据,所以在上面的集合中,它是唯一一个安全的方法。不管你调用多少次基于REST的GET方法,它永远不会改变数据库中的任何东西。但是,这并不是该动作的固有特性,而是关系到你该如何实现它,所以我们需要确保它是这样运行的。所有其他的方法都会以不同的方式改变数据,不能随意使用。在REST中,GET方法既是安全的,又是幂等的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“幂等”的方法指的是多次使用不会产生不同结果的方法。按照REST,DELETE方法应该是幂等的,如果删除了某个资源,然后针对相同的资源再次调用DELETE,它不会改变任何东西。资源应该早就已经消失了。在REST规范中,POST是唯一一个非幂等的方法,所以我们可以对相同的资源多次调用POST方法,这样我们会得到重复的资源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们重新看一下Slack样例,如果我们使用HTTP动作来进行更多的操作会是什么样子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/dc\/dc65f5123e4d75a5ca693c3250e1e00d.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们可以使用POST方法发送消息到通用的通道,我们也可以使用GET方法从通用通道获取消息。我们还可以使用DELETE方法和特定的ID删除消息,这里比较有意思的一点在于,消息并不是与特定通道关联的,所以我可以设计一个单独的API来删除资源。这个例子表明,设计API并不总是那么简单,这方面有很多可选项和权衡。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 3:HATEOAS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"还记得纯文字、没有任何图像的电脑游戏吗?我们只能看到一些文本,描述了你在哪里,以及接下来能干什么。为了取得进展,我们必须要输入自己的选择。在一定程度上来讲,HATEOAS就是做这件事情的。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/84\/84ae732d51f4136e5ebc8112e81ccd3d.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HATEOAS指的是“超媒体作为应用状态引擎(Hypermedia as the Engine of Application State)”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了HATEOAS之后,当其他人使用你的API的时候,他们就能看到通过API还能做哪些其他的事情。HATEOAS回答了“从这里出发,我还能去哪里?”的问题。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/32\/324ea9a1cdc56cbf5ceb14560c68bf22.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但这还不是所有的内容。HATEOAS还可以对数据关系进行建模。我们可能会有一个关于图书的资源,并且在URL中没有将作者信息嵌套进来,但是我们可以包含它们的链接,如果有人对作者感兴趣的话,那么他们可以访问这些链接并探索相关的数据。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HATEOAS不像其他成熟度模型的等级那样流行,但是有些开发人员确实在使用它。其中一个样例就是Jira,如下是它们的搜索API的响应:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a3\/a3ba14e0a106e4cb3c5885c78490e600.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"他们将链接嵌入到了其他我们可以探索的资源中,以及该issue的状态过渡列表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一个使用HATEOAS的样例是Artsy。他们的API严重依赖HATEOAS,并且还使用了JSON Plus调用规范,按照该规范强制要求使用一种特殊的约定来构建链接。下面是一个分页的例子,这是使用HATEOAS最酷的样例之一:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a0\/a0c07b91635d474de3150f423fdfc3de.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们可以提供到下一页、上一页、第一页和最后一页的链接,还可以按照需要添加其他页面的链接。这样简化了API的消费,因为这样不需要在客户端添加URL的解析逻辑,也不需要追加页码的方法。我们只需要在客户端使用已经实现结构化的链接就可以了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"好的API由什么组成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们已经介绍完了Richardson模型,但这并不是实现好的API的全部内容。其他重要的品质还有什么呢?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"错误\/异常处理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我对自己使用的API的基本期望之一就是,需要有一种明确的方式来判断是否有错误或异常。我想要知道请求是否得到了处理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTTP有一种简单的方式来实现这一点:HTTP状态码。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"管理状态码的基本规则是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2xx代表一切正常"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3xx代表你想要找的公主在另外一个城堡,也就是你要找的资源在其他的地方"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4xx代表客户端做错了某些事情"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5xx代表服务器端失败"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1d\/1de1292432698877e45fc756952f818e.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们的API至少要提供4xx和5xx状态码。有时候,5xx是自动生成的。例如,客户端发送了一些内容到服务器端,但是这非法的请求,而我们的校验是有缺陷的,从而导致这个问题继续在代码中执行了下去,最终导致出现了异常,这样就会返回一个5xx的状态码。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你想要承诺使用特定的状态码,那么你会遇到“哪种状态码最适合当前情况?”的问题。这样的问题并不总是那么容易回答,我推荐你去阅读声明这些状态码的RFC,它们给出了比其他来源更广泛的解释,并且告诉了你何时使用这些状态码更合适等。幸运的是,网上有些资源可以帮助我们做出选择,比如"},{"type":"link","attrs":{"href":"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Status","title":"","type":null},"content":[{"type":"text","text":"Mozilla的HTTP状态码指南"}]},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"文档"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"优秀的API必须要有优秀的文档。在文档方面,最大的问题在于,随着API的发展需要找人同步更新文档。有个更好的方案是不脱离代码自更新文档。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,注释与代码的脱节。当代码发生变化的时候,注释依然保持不变,这样的话,注释就过时了。这甚至会比根本就没有任何注释更糟糕,因为在随后的一段时间内,它们会提供错误的信息。注释不会自动更新,所以开发人员需要记得在维护代码的时候同时维护它们。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自更新的文档工具可以解决这个问题。在这方面,一个流行的工具就是Swagger,它是基于OpenAPI构建的工具,可以很容易地描述你的API。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a0\/a0c886829cda281fe0e67fc847ded427.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Swagger很酷的一点在于它是可执行的,所以如果你尝试修改API,能立即看到它的作用和变化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了给Swagger添加自动更新功能,我们需要使用其他的插件和工具。在Python中,有针对大多数主流框架的插件。它们能生成API请求该如何组织的描述,并定义数据的输入和输出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你不想要使用Swagger,而是想使用更简单的工具,那该怎么办呢?有个流行的替代方案是"},{"type":"link","attrs":{"href":"https:\/\/slatedocs.github.io\/slate\/#introduction","title":"","type":null},"content":[{"type":"text","text":"Slate"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/04\/04cf2a7531c70aa7f2e1b57e6f94926f.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"还有一些值得推荐的中间方案,如"},{"type":"link","attrs":{"href":"https:\/\/github.com\/Mermade\/widdershins","title":"","type":null},"content":[{"type":"text","text":"widdershins"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/api2html.com\/docs\/overview\/","title":"","type":null},"content":[{"type":"text","text":"api2html"}]},{"type":"text","text":"的组合,它允许我们从Swagger的定义中生成类似Slate的文档。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"缓存"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在有些系统中,缓存可能并不是什么大问题。这样的系统可能没有很多的数据可供缓存,所有的数据都在不断地发生变化,或者系统根本没有很大的流量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,在大多数情况下,缓存对于良好的性能至关重要。它与RESTful API密切相关,因为HTTP协议在缓存方面做了很多事情,比如HTTP头信息允许我们控制缓存的行为。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能想要在客户端缓存东西,或者如果有注册表或值存储的话,那么你可能想要在应用程序中缓存数据。但是,HTTP让我们能够基本上免费就可以获得一个很好的缓存,所以如果可能的话,请不要错过这个免费的午餐。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ea\/ea51ea91c288ba5aa0a4d3c001071f2e.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同时,因为缓存是HTTP规范的一部分,所以很多涉及HTTP的技术都知道如何进行缓存:浏览器原生支持缓存,客户端和服务器之间的中间技术也是如此。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"API设计的演化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"构建API以及现代软件最重要的部分就是适应性。如果没有适应性,开发就会变慢,在合理的时间发布特性就会变得更加困难,当面对最后截止时间的时候更是如此。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“软件架构”在不同的上下文语境中有不同的含义,不过我们现在采用这个定义:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"软件架构一种行为\/艺术,能够避免会阻碍未来变化的决策。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"记住了这一点,在设计软件的时候,当你必须要在具有相似优点的方案中做出选择时,你应该始终选择更多考虑到未来的方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好的实践并不是万能的。按照正确的方式构建错误的东西并不是你想要的结果。最好采取一种成长的心态,接受变化是不可避免的,尤其是如果你的项目要持续发展的话更是如此。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要想让你的API更具适应性,其中很关键的一点就是保持尽可能薄的API层,真正的复杂性应该往下层转移。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"API不应该限定实现"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"公开的API发布之后,它就已经完成了,是不可改变的,你就不能再去触碰它了。如果你已经有了一个设计古怪的API,除了接受现状之外,还能做些什么呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你应该不断寻找简化实现的方法。有时候,你可以通过一个特定的HTTP头信息来控制API响应的格式,相对于构建另外一个叫做v2的新API,这是一种更简单的解决方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"API只是另外一层的抽象。它们不应该决定如何实现,为了避免这种问题,我们可以采用如下几种开发模式。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"API网关"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这是一种类似于门面的开发模式。如果你要把一个单体结构拆分为一组微服务,并且希望向外部暴露一些功能的话,那么你只需要构建一个类似门面的API网关。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它将为不同的微服务提供一个统一的接口(这些微服务可能有不同的API,使用不同的错误格式等等)。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"适用于前端的后端"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你必须要构建一个API来满足一堆不同的客户端的话,那么这可能会非常困难。针对某个客户端所作出的决策可能会影响其他客户端的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照适用于前端的后端(backend for frontend)理念,如果你有不同的客户端,它们喜欢不同形式的API,比如移动应用可能会喜欢使用GraphQL,那么就单独为它们构建吧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只有当你的API是一层抽象,并且这个抽象层很薄的时候,这种方式才有效。如果它与你的数据库耦合,或者太大,具有太多的逻辑,那么就无法这样做了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GraphQL与RESTful"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很多人都在热炒GraphQL。它是一项新兴的技术,但是已经有了很多粉丝,以至于有些开发者声称它将取代REST。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"尽管GraphQL比RESTful要新的多,但是它们有很多相似之处。GraphQL最大的不足之处在于它的缓存,它必须要在客户端或应用程序中实现。现在,有内置的实现了缓存功能的客户端库(比如"},{"type":"link","attrs":{"href":"https:\/\/www.apollographql.com\/docs\/react\/caching\/cache-configuration\/","title":"","type":null},"content":[{"type":"text","text":"Apollo"}]},{"type":"text","text":"),但是这仍然要比使用HTTP提供的几乎免费的缓存功能要困难。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从技术讲,GraphQL位于Richardson模型的Level 0层级,但是它具有良好API的特质。我们可能无法同时使用多个HTTP的功能,但是GraphQL的出现就是解决这一问题的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL的杀手鐗就是聚合不同的API,并将它们作为一个GraphQL API暴露出来。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b7\/b7c49c32c9a2c71be62d1111ac5b7973.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL在处理数据抓取不足和数据过量抓取方面有很好的效果,而这些问题是REST API很难进行管理的。这两个问题都与性能有关,如果数据抓取不足,那说明你没有高效地使用API,所以必须要进行大量的调用。如果数据过量抓取的话,那么API调用的数据传输会比必要的数据传输更大,这是对带宽的一种浪费。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小结"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"借助REST与GraphQL的比较,我们能够总结出一个好的API最重要的品质。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好的API的特性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/34\/34ed63f23989861a74525adfaeb63a60.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们需要一个清晰的数据表述方式:RESTful以资源的方式提供了表述。我们需要有一种方式显示有哪些可用的操作:RESTful通过组合资源和HTTP动作实现这一点。我们需要有一种方式来确认是否存在错误\/异常:HTTP状态码可以实现这一点,可能还会包含阐述它们的响应信息。最好能够提供API发现和导航的功能:在RESTful中,HATEOAS负责实现这一点。有好的文档是非常重要的:在这方面,可执行、自更新的文档可以解决这个问题,这超出了RESTful规范的范围。最后,但同样重要的是,优秀的API应该具有缓存功能,除非你的特定情况认为它是不必要的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST和GraphQL之间最大的区别是它们处理缓存性的方式。当我们使用REST方式构建API的时候,我们基本上可以免费获得HTTP的缓存功能。如果选择GraphQL的话,你需要自行负责为客户端或应用程序添加缓存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文链接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/www.stxnext.com\/blog\/how-to-build-a-good-api-that-wont-embarrass-you"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章