首发于极乐科技

【spring 指南系列】如何更好的设计RESTful API

当您的数据模型已开始稳定,您可以为您的网络应用程序创建公共API。 你意识到,很难对你的API进行重大更改,一旦它发布,并希望尽可能得到尽可能多的前面。 现在,互联网对API设计的意见有很多。 但是,因为没有一个广泛采用的标准在所有情况下都有效,所以你前面有一堆选择:你应该接受什么格式? 你应该如何认证? 你的API是否应该版本化?

构建API是您可以做的最重要的事情之一,以提高您的服务的价值。 通过使用API,您的服务/核心应用程序有可能成为其他服务增长的平台。 看看当前巨大的科技公司:Facebook,Twitter,谷歌,GitHub,亚马逊,Netflix …没有一个人会像今天一样大,如果他们没有通过API打开他们的数据。 事实上,整个行业存在的唯一目的是消费由所述平台提供的数据。

你的API越简单明了,使用的人就越多。

许多在网络上发现的API设计观点是围绕主观的模糊标准解释的学术讨论,而不是在现实世界中有意义的。 我的目标是描述一个为当今的Web应用程序设计的务实的API的最佳实践。 我没有尝试满足一个标准,如果它不觉得正确。 为了帮助指导决策过程,我写了一些API必须努力达到的要求:

  • 它应该使用Web标准,他们有意义
  • 它应该对开发人员友好,可以通过浏览器地址栏探索
  • 它应该简单,直观和一致,使采用不仅容易,而且愉快
  • 它应该提供足够的灵活性来支持大部分我们所设计的UI
  • 应该是有效的,同时保持与其他要求的平衡

API是开发人员的UI - 就像任何UI一样,确保用户的体验被仔细考虑是非常重要的!

RESTful API设计定义

以下是我将在本文档中使用的一些重要术语:

  • Resource :对象的单个实例。 例如,一只动物。
  • 集合:对象的集合。 例如,动物。
  • HTTP :用于通过网络通信的协议。
  • Consumer :能够发出HTTP请求的客户端计算机应用程序。
  • 第三方开发人员:不是您项目的一部分,但希望使用您的数据服务的开发人员。
  • 服务器:可通过网络从客户端访问的HTTP服务器/应用程序。
  • 端点:服务器上的API网址,表示资源或整个集合。
  • 幂等:无边际效应,多次操作得到相同的结果。
  • 网址区段:网址中的斜线分隔的信息。

数据设计和抽象

首先将从你写的开发文档API开始(比如我们可以看到各个开发平台的暴露出来的API文档),您需要决定如何设计数据,以及您的核心服务/应用程序如何工作。 如果你在做的API是第一次开发,这应该很容易。 如果您要将API附加到现有项目,则可能需要提供更多抽象(毕竟是要按照已有的文档规范来做)。

有时,集合可以表示数据库表,资源可以表示该表中的一行。 然而,这不是通常的情况。 事实上,你的API应该尽可能多地抽象出你的数据和业务逻辑。 非常重要的一点是,如果您不希望使用你的API很难使用,就不要使用任何复杂的应用程序数据来为难第三方开发人员(让开发人员觉得还得对这些数据进一步处理而浪费更多精力)。

还有你的服务的很多部分,你不应该通过API公开。 一个常见的例子是许多API不允许第三方创建用户。

设计资源请求

当然你知道GET和POST请求。当您的浏览器访问不同的网页时,这两个最常用的请求。POST是如此受欢迎,它甚至流行语我们的平常的说话中,即使那些不知道互联网如何工作的人也知道他们可以“发布”的东西在朋友的Facebook上。

有四个半非常重要的HTTP动词,你需要知道。我说“一半”,因为PATCH动词非常类似于PUT动词,两个通常由许多API开发人员组合。这里是动词,在他们旁边是他们相关的数据库调用(我假设大多数人读这个知道更多关于写入数据库而不是设计一个API)。

  • GET (SELECT):从服务器检索特定资源,或资源列表。
  • POST (CREATE):在服务器上创建一个新的资源。
  • PUT (UPDATE):更新服务器上的资源,提供整个资源。
  • PATCH (UPDATE):更新服务器上的资源,仅提供更改的属性。
  • DELETE (DELETE):从服务器删除资源。

这里有两个较少知名的HTTP动词:

  • HEAD - 检索有关资源的元数据,例如数据的哈希或上次更新时间。
  • OPTIONS - 检索关于客户端被允许对资源做什么的信息。

一个好的RESTful API将使用四个半HTTP动词,允许第三方与其数据进行交互,并且不会将动作/动词作为URL段。

通常,GET请求可以被缓存(通常是!)在浏览器,例如将缓存请求头用于第二次用户的POST请求。 HEAD请求基本上是一个没有响应主体的GET,并且也可以被缓存。

版本控制

无论你正在构建什么,无论你事先做了多少规划,你的核心应用程序总会改变,你的数据关系总会改变,属性添加和从你的资源中删除。这只是软件开发的工作原理,尤其是如果你的项目还活着并被许多人使用(如果你正在构建一个API,情况可能就会如此)。

记住,API是服务器和客户端之间的已发布约定。如果您更改了服务器API,这些更改会破坏向后兼容性,那么你就打破了这个约定,客户端又会要求你重新支持它(谁让客户端依然是之前的版本,调用的还是之前的API)。为了避免这样的事情,并让您的客户端满意,您需要偶尔引入新版本的API,同时仍允许访问旧版本。

注意,如果你只是为你的API添加新的特性,例如资源上的新属性,或者如果你添加新的端点(比如之前只有查询,现在增加一个修改),你不需要增加您的API版本号,因为这些更改不会破坏向后兼容性。当然,您将需要更新您的API文档。

随着时间的推移,您可以弃用API的旧版本。弃用某个功能并不意味着关闭它或者降低它的质量,而是告诉客户端您的API,旧版本将在特定日期删除,并且他们应该升级到较新的版本。

一个好的RESTful API设计将跟踪URL中的版本。另一个最常见的解决方案是将版本号放在请求头中,但在与许多不同的第三方开发人员合作之后,我可以告诉您,添加这些请求头信息并不像添加网址细分那么容易。

分析

跟踪客户端使用的API的版本/端点。 这可以像每次请求时在数据库中增加一个整数一样简单。 有很多原因跟踪API Analytics是一个好主意,例如,最常用的API调用应该是高效的。

为了构建第三方开发者所喜欢的API,最重要的是,当您弃用某个版本的API时,实际上可以使用已弃用的API功能与开发人员联系(在两个异构系统中当对方的开发人员调用本服务时顺带告知对方)。 这是提醒他们在弃用旧API版本之前升级的完美方法。

第三方开发者通知的过程可以自动化,例如。 每当对一个已弃用的功能发出10,000个请求时,发邮件通知开发人员。

API Root URL

无论你相信与否,您的API的根位置是重要的。当开发人员使用您的API接手旧项目并需要构建新功能时,他们可能根本不知道您有哪些服务。幸好他们知道客户端对外调用的那些URL列表。重要的是,进入您的API的根入口点尽可能简单,因为长的复杂URL将显得令人生畏,并可能使开发人员直接略过而不会采用。

这里有两个常见的URL根:

如果您的应用程序庞大,或者您预计它会变得庞大,将API放在自己的子域(例如 api。)上是一个不错的选择。这可以允许在路上一些更灵活的可扩展性。

如果您预计您的API将不会增长到那么大,或者您想要一个更简单的应用程序设置(例如,您希望从同一个框架托管网站和API),将您的API放置在域根的URL段(例如 / api / )也有效。

将内容设为您的API根目录是个好主意。例如,点击GitHub的API的根会返回一个端点列表。就个人而言,我喜欢使用根网址提供给开发人员认为有用的信息,例如,如何获取API的开发人员文档。

此外,请注意HTTPS前缀。作为一个好的RESTful API,您必须在HTTPS之后托管您的API(一个好的RESTful API总是基于HTTPS来发布的)。

端点

端点是您的API中指向特定资源或资源集合的URL。

如果你正在构建一个虚拟的API来代表几个不同的动物园,每个动物园包含许多动物,员工(可以在多个动物园工作)和跟踪每个动物的物种,你可能有以下端点:

当引用每个端点可以做什么时,您需要列出有效的HTTP动词和端点组合。例如,这里有一个半全面的行动列表,可以使用我们虚构的API执行。请注意,我在每个端点之前都有HTTP动词,因为这是在HTTP请求标头中使用的相同符号。

  • GET / zoos:列出所有动物园(ID和名称,不要太多细节)
  • POST / zoos:创建一个新的Zoo
  • GET / zoos / ZID:检索整个Zoo对象
  • PUT / zoos / ZID:更新Zoo(整个对象)
  • PATCH / zoos / ZID:更新Zoo(部分对象)
  • DELETE / zoos / ZID:删除动物园
  • GET / zoos / ZID / animals:检索动物列表(ID和名称)。
  • GET / animals:列出所有动物(ID和名称)。
  • POST / animals:创建一个新的动物
  • GET / animals / AID:检索动物对象
  • PUT / animals / AID:更新动物(整个对象)
  • PATCH / animals / AID:更新动物(部分对象)
  • GET / animal_types:检索所有动物类型的列表(ID和名称)
  • GET / animal_types / ATID:检索整个动物类型对象
  • GET / employees:检索完整的员工列表
  • GET / employees / EID:检索特定员工
  • GET / zoos / ZID / employees:检索在此动物园工作的员工(ID和名称)的列表
  • POST / employees:创建一个新员工
  • POST / zoos / ZID / employees:在特定动物园雇用员工
  • DELETE / zoos / ZID / employees / EID:从特定的动物园中解雇员工

在上面的列表中,ZID表示Zoo ID,AID表示动物ID,EID表示Employee ID,ATID表示动物类型ID。在你的文档中有一个键,你选择的任何约定是一个好主意。

为了简洁,我在上面的示例中省略了常见的API网址前缀。虽然这在通讯期间可能很好,但在实际的API文档中,您应该始终显示每个端点的完整网址(例如GET http://api.example.com/v1/animal_type/ATID)。

注意数据之间的关系如何显示,特别是雇员和动物园之间的多对多关系。通过添加其他网址细分,您可以执行更具体的互动。当然,对于“FIRE(解雇)”没有HTTP动词,但是通过对位于Zoo内的Employee执行DELETE,我们能够实现相同的效果。

过滤器

当客户端请求对象列表时,请务必为它们提供符合所请求条件的每个对象的列表。这个列表可能是巨大的。但是,重要的是不要对数据执行任何任意限制。正是这些任意的限制使第三方开发者很难知道发生了什么。如果他们请求某个集合,并迭代结果,他们从来没有看到超过100个结果,接下来他们就不得不去查找这个限制条件的出处(提供服务端没有问题,就只能是调用端的问题了)。到底是他们的ORM的bug导致的,还是因为网络截断了大数据包?

尽可能减少那些会影响到第三方开发者开发的无谓限制

然而,重要的是,您确实为客户端提供了指定某种过滤/结果限制的能力。这么做最重要的一个原因是可以最小化网络传输,客户端尽快得到结果。第二个重要的原因是客户端可能是懒惰的,如果服务器可以为他们做过滤和分页,一切都更好。还有一个不那么重要的原因,请求资源越少,对服务器的一个很大的好处是,减少了负载。

过滤主要用于对资源集合执行GET。由于这些是GET请求,因此应通过URL传递过滤信息。以下是您可能想要添加到API的过滤类型的一些示例:

  • ?limit = 10:减少返回给Consumer的结果数(用于分页)
  • ?offset = 10:向客户端发送信息集(用于分页)
  • ?animal_type_id = 1:过滤符合以下条件的记录(WHERE animal_type_id = 1)
  • ?sortby = name&order = asc:根据指定的属性对结果进行排序(ORDER BYname ASC)

其中一些过滤可能与端点URLS冗余。例如我之前提到的GET / zoo / ZID / animals。这与GET / animals是一样的吗?zoo_id = ZID。为客户端提供的专用端点将使他们的开发更轻松,这对于您预期他们会做很多的请求尤其如此。在文档中,提及这种冗余,以便第三方开发人员不会留意是否存在差异。

还有一个要说的是,每当您执行数据的过滤或排序时,请确保您列出客户端可以过滤和排序的列。我们不希望将任何数据库错误发送给客户端!

状态码

作为RESTful API,使用正确的HTTP状态代码非常重要;他们是一个标准!各种网络设备能够读取这些状态码,例如,负载平衡器可以配置为避免向发送大量50x错误的Web服务器发送请求。有很多HTTP状态代码可供选择,但此列表应该是一个很好的起点:

  • 200 OK - [GET]
  • 客户端从服务器请求数据,服务器为它们找到它(等幂)
  • 201 CREATED - [POST / PUT / PATCH]
  • 客户端提供了服务器数据,并且服务器创建了一个资源
  • 204 无内容 - [删除]
  • 客户端要求服务器删除资源,并且服务器将其删除
  • 400 无效请求 - [POST / PUT / PATCH]
  • 客户端给服务器的数据不良,服务器没有做任何事情(幂等)
  • *错误404 - []
    *客户端引用了一个不存在的资源或集合,并且服务器什么也不做(幂等)
    
  • *500内部服务器错误 - []
    *服务器遇到错误,并且客户端不知道请求是否成功
    

状态码范围

1xx 范围保留用于底层HTTP的东西,你很可能永远也用不到。

2xx 范围保留用于成功消息,尽可能确保您的服务器尽可能多地向客户端发送这些消息。

3xx 范围保留用于重定向。大多数API不使用这些请求很多(不像SEO人使用它们那么频繁),然而,较新的超媒体风格API将更多地使用这些请求。

4xx 范围保留用于响应客户端做出的错误,例如。他们提供不良数据或要求不存在的东西。这些请求应该是幂等的,而不是更改服务器的状态。

5xx 范围的状态码是保留给服务器端错误用的。这些错误常常是从底层的函数抛出来的,甚至开发人员也通常没法处理,发送这类状态码的目的以确保客户端获得某种响应。当收到5xx响应时,客户端不可能知道服务器的状态,所以这类状态码是要尽可能的避免。

预期的返回文档

当使用不同的HTTP动词对服务器端点执行操作时,客户端需要在返回结果里面拿到一系列的信息。下面的列表是非常典型的RESTful API:

  • GET / collection:返回资源对象的列表(数组)
  • GET / collection / resource:返回单个Resource对象
  • POST / collection:返回新创建的Resource对象
  • PUT / collection / resource:返回完整的Resource对象
  • PATCH / collection / resource:返回完整的Resource对象
  • DELETE / collection / resource:返回一个空文档

请注意,当Consumer创建资源时,他们通常不知道正在创建的资源的ID(也不知道其他属性,如创建和修改的时间戳)(如果适用)。 这些附加属性与后续请求一起返回,当然作为对初始POST的响应。

认证

大多数时候,一个服务器想要知道谁正在做哪些请求。当然,一些API提供公共用户(匿名用户)使用的,但大多数时间的工作是代表某人执行。

OAuth 2.0提供了一个很好的方法。对于每个请求,您可以确定知道哪个客户正在发出请求,代表他们请求哪个用户,并提供一种(大部分)标准化的方式来过期访问或允许用户撤消来自客户端的访问权,需要第三方客户端知道用户登录凭据。

还有OAuth 1.0xAuth同样适用这样的场景。无论您选择哪种方法,请确保它是常见的,并且有许多不同的库为您的客户端可能使用的语言/平台编写的文档(比如redis提供Java调用的API)。

我可以诚实地告诉你,OAuth 1.0a,虽然它是最安全的选项,但是实现起来很痛苦。建议你选择一个替代品。

内容类型

目前,最令人兴奋的API提供来自RESTful接口的JSON数据。这包括Facebook,Twitter,GitHub,你命名。 XML似乎已经失去了优势(除了在大型企业环境中)。 SOAP,不幸的是,它过时了,我们真的没有看到太多的API把HTML作为结果返回给客户端(除非你在构建一个爬虫程序)。

只要你返回给他们有效的数据格式,开发者就可以使用流行的语言和框架进行解析。如果你正在构建一个通用的响应对象并使用不同的序列化器,你也可以很容易的提供之前所提到的那些数据格式(不包括SOAP)。而你所要做的就是把使用方式放在响应数据的接收头里面。

一些API创建者建议向URL(端点之后)添加.json,.xml或.html文件扩展名以指定要返回的内容类型,但我个人不喜欢这一点。我真的很喜欢Accept头(它是内置在HTTP规范),并且我觉得这么做也比较适当一些。

超媒体API

超媒体API很可能是RESTful API设计的未来。 实际上是一个非常好的概念,它回归到了HTTP和HTML如何运作的“本质”。

当使用非超媒体RESTful API时,URL端点是服务器和使用者之间的约定的一部分。这些端点必须由客户端提前知道,并且更改这些端点意味着客户端不再能够按预期与服务器通信。你可以先假定这是一个限制。

现在,API客户端已经不仅仅只有那些创建HTTP请求的用户代理了。大多数HTTP请求是由人们通过浏览器产生的。人们不会被哪些预先定义好的RESTful API端点URL所约束。是什么让人们变的如此与众不同?人们可以阅读内容,点击链接,看看有趣的标题,一般来说,探索一个网站,解释内容,去他们想去的地方。即使一个URL改变,人们也不受影响(除非,他们事先给某个页面做了书签,在这种情况下,他们去主页并发现原来有一条新的路径可以去往之前的页面)。

超媒体API概念的工作方式与人类相同。请求API的根返回一个URL列表,它可能指向每个信息集合,并以客户端可以理解的方式描述每个集合。为每个资源提供ID并不重要(或必需),只要提供了一个URL即可。

随着超媒体API的客户端爬行链接和收集信息,URL在响应中始终是最新的,并且不需要事先知道作为约定的一部分。如果URL被缓存,并且后续请求返回404,则客户端可以简单地返回到根并再次发现内容。

在检索集合中的资源列表时,将返回包含各个资源的完整URL的属性。当执行POST / PATCH / PUT时,响应可以是3xx重定向到完整的资源。

JSON不仅告诉了我们需要定义哪些属性作为URL,也告诉了我们如何将URL与当前文档关联的语义。正如你猜的那样,HTML就提供了这样的信息。我们可能很乐意看到我们的API走完了完整的周期,并回到了处理HTML上来。想一下我们与CSS一起前行了多远,有一天我们甚至可能会看到,API和网站使用完全相同的URL和内容是常见的做法。

文档

老实说,即便你不能百分之百的遵循指南中的条款,你的API不一定是糟糕的。但是,如果你不为API准备文档的话,没有人会知道如何使用它,那它真的会成为一个糟糕的API。

使您的文档可用于未经身份验证的开发人员。

不要使用自动文档生成器,或者如果你这样做,你也要保证自己审阅过并使其具有更好的版式。

不要截断示例请求和响应正文,要展示完整的东西。在文档中使用语法高亮指示符。

记录每个端点的预期响应代码和可能的错误消息,以及导致这些错误消息可能出现的错误。

如果您有空闲时间,请构建一个开发人员API控制台,以便开发人员可以立即试用您的API。这不像你想象的那么难,开发者(内部和第三方)也会因此而拥戴你!

确保您的文档可以打印; CSS是一个强大的东西;不要害怕在打印文档时隐藏侧边栏。即使没有人打印过物理副本,你会惊奇的发现有多少开发者喜欢打印到PDF以供离线阅读。

勘误:原始的HTTP封包

因为我们所做的一切都是通过HTTP,我将向你展示一个HTTP包的剖析。 我经常感到惊讶的是,有多少人不知道这些东西是什么样子的! 当客户端向服务器发送请求时,它们提供一组键/值对,称为标题,以及两个换行符,最后是请求体。 这都是在同一个数据包中发送的。

服务器然后以所述键/值对格式,用两个换行符然后响应主体进行响应。 HTTP是一个请求/响应协议; 没有“推送”支持(服务器向客户端发送数据未经安全),除非您使用不同的协议,如Websockets。

在设计API时,您应该能够使用允许查看原始HTTP数据包的工具。 例如,考虑使用Wireshark。 此外,请确保您使用的框架/ Web服务器,允许您阅读和更改尽可能多的这些字段。

Example HTTP Request

POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24

{
  "name": "Gir",
  "animal_type": 12
}

Example HTTP Response

HTTP/1.1 200 OK
Date: Wed, 18 Dec 2013 06:08:22 GMT
Content-Type: application/json
Access-Control-Max-Age: 1728000
Cache-Control: no-cache

{
  "id": 12,
  "created": 1386363036,
  "modified": 1386363036,
  "name": "Gir",
  "animal_type": 12
}

参考文章:

Principles of good RESTful API Design - Code Planet
Best Practices for Designing a Pragmatic RESTful API

本人同步博客:一叶知秋

编辑于 2017-01-06 14:41