本章是我们的API之旅的转折点。我们已经结束了基础部分的浏览,准备来看看之前的概念如何与构建API结合起来。在这一章,我们通过设计一个API来讨论API的各个组件。
组织数据
国家地理杂志2011年估计,美国人拍摄了800亿张照片。这么多的照片,你可以想象人们会使用各种不同的方法来组织他们电脑上的照片。一些人会选择全部丢在一个文件夹里。另一些人会细心的将照片放到不同的文件夹,这些文件夹根据年,月和事件的层次关系进行组织。
公司在构建API的时候也会进行类似的考虑。就像我们在第一章提到的,API的目的是让电脑方便的处理公司的数据。处于易用性的考虑,公司可能会决定对所有的数据使用同一个URL,然后让它可以进行搜索(类似于将所有照片放在同一个文件夹里)。另一些公司可能决定数据的每一部分有自己的URL,组织成一个层次关系(就像使用目录和子目录来管理照片)。每一个公司在业界已有的最佳实践的指导下,根据自己的情况选择最合适的方式来组织自己的API。
从架构风格开始
当讨论API的时候,你可能会听到他们谈论“soap”和“rest”,或许你会奇怪这些软件开发者是在工作还是在计划度假。事实上这是两个基于web的API的最常用的架构的名字。SOAP(首字母的缩写)是一个基于XML的设计,它的请求和响应有标准的格式。REST代表表述性状态传递,是一种更开放的方式,它提供了很多约定,但也给API的设计者留了很多自己做决定的空间。
纵观本课程,你可能已经注意到我们更倾向于REST API。这种偏好很大程度上是由于REST那惊人的占有率。这并不是说SOAP不好,它也有自己的优势。然而,我们讨论的焦点仍会保持在REST上,因为你遇到的API很可能是REST类型的。在接下来的部分,我们将谈论构成REST API的组件。
我们的第一个资源
回到第二章,我们谈论过一点资源。回忆一下,资源就是API中的名词(顾客和披萨)。这些是我们希望可以通过我们的API进行处理的东西。
为了感受公司如何设计一个API,我们从披萨店开始试试手。我们将从增加订披萨的功能开始。
为了让客户端可以和我们讨论披萨,我们需要做这些事情:
- 确定哪些资源是可访问的
- 为这些资源分配URL
- 确定允许客户端对这些资源进行哪些操作
- 估计每个动作需要数据的哪些部分,这些数据应该采用什么格式
第一个任务挑选资源就有些难度。解决这个问题的一个方法是逐步深入了解一个典型的交互需要涉及哪些资源。对于披萨店来说,我们可能会有一个菜单。菜单上是各种披萨。当顾客希望我们为他们做一个披萨的时候,他们下一个订单。在这个背景下,菜单、披萨、顾客和订单听起来都是备选的资源。让我们从订单开始。
下一个步骤是为资源分配URL。可选的方案有很多,幸运的是REST的规定可以提供一些指导。在一个典型的REST API中,一个资源会有两个分配给它的URL模式。第一个是资源名字的复数形式,比如 /orders。第二个是资源名字的复数加上一个唯一的标识符来指定一个资源,比如/orders/<order_id>,其中<order_id>是某一个订单的唯一标识符。这两种URL模式构成了我们的API将会支持的第一个终端。叫它们终端仅仅是因为他们处在一个URL的末尾,比如在 http://example.com/<endpoint_goes_here> 。
既然我们挑选了资源,也为它们分配了URL,我们就需要决定客户端可以执行哪些动作。根据REST的规则,我们说复数终端(/orders)是为了列出所有存在的订单和创建新的订单。带有唯一标识符的复数终端(/orders/<order_id>),是为了获取、更新或者取消某个特定的订单。客户端通过请求中适当的HTTP动词 (GET, POST, PUT 或者DELETE)告诉服务器要执行哪个动作。
总体来说,现在我们的API看起来是这样的:
HTTP动词 | 终端 | 动作 |
---|---|---|
GET | /orders | 列出已有的订单 |
POST | /orders | 添加一个新的订单 |
GET | /orders/1 | 获取订单 #1的详情 |
GET | /orders/2 | 获取订单 #2的详情 |
PUT | /orders/1 | 更新订单 #1 |
DELETE | /orders/1 | 取消订单 #1 |
有了对于订单终端的详实的动作,最后要做的就是决定客户端和服务器之间要交换哪些数据。借用第3章披萨店的例子,我们可以说一个订单需要外壳类型和配料。我们还需要选择一个客户端和服务器之间传递信息的数据格式。XML和JSON都是很好的选择,但是对于可读性的考虑,我们会选择JSON。
到这个时候,你可以松口气了,我们已经设计了一个实用的API。下面是客户端和服务器使用这个API进行交互的例子:
把资源联系在一起
我们的披萨店的API看起来不错,订单前所未有的多。事实上,生意确实不错,我们决定开始追踪顾客的订单来估计顾客的忠诚度。一个简单的方式是增加一个新的顾客资源。
和订单资源一样,我们的顾客资源需要一些终端。根据规定, /customers和/customers/<id>非常合适。我们将会跳过细节,但是我们要说我们决定了对于每个终端来说哪些动作是有意义的,以及哪些数据代表了一个顾客。假设我们已经完成了这些,我们就遇到了一个有趣的问题:我们怎么把顾客和订单联系起来呢?
REST的实践者们在如何解决联系资源的问题上发生了分歧。一些人认为这种层次结构需要增长,增加终端,比如/customers/5/orders作为订单的所有顾客#5,/customers/5/orders/3作为顾客#5的第三个订单。另一些人认为应该在资源的数据中增加联系的细节来保持结构的扁平。在这种模型下,创建一个订单需要跟订单细节一个传递一个customer_id字段。这两种方案都在使用,所以都需要了解。
由于系统中数据的增长,列举所有订单的终端就变得不切实际了。假设我们的披萨店有三百万已完成的订单,你想找出有多少用意大利辣香肠作为配料。向/orders发送一个 GET 请求,并且获取全部的三百万订单就不太合适了。幸好,REST有一个实用的方法在数据中进行搜索。
URL还有另一个我们还没有提到过的组成部分,查询字符串。查询就是搜索,字符串就是文本。查询字符串是URL末尾传递东西到API的少量文本。比如这个URL中问号后面的所有东西都是查询字符串 http://example.com/orders?key=value 。
REST API使用查询字符串来定义查询的细节。这些细节被称为查询参数。API决定它接受哪些参数,以及为了使查询有效应该为这些参数使用的准确的名字。我们的披萨店API允许客户端使用这个URL来按照配料进行查询: http://example.com/orders?topping=pepperoni 。客户端可以包含多个查询参数,只要将这些参数一个接一个的列出来,中间用and符号(“&”)分隔。比如 http://example.com/orders?topping=pepperoni&crust=thin 。
查询字符串的另一个用途是限制每个请求返回的数据的数量。通常,API会将结果分组(每组100或500条记录),每次返回一组。这种分割数据的处理就是我们知道的分页(类似于书的分页)。为了允许客户端访问数据的任一页,API会支持一个查询参数,这个参数允许客户端指定它想获取数据的哪一页。在披萨店API中,我们通过允许客户端指定两个参数:page和size来支持分页。如果客户端发送这样的请求 GET /orders?page=2&size=200 我们就知道他们需要结果的第二页,煤业包含200条记录,也就是订单201-400。
译自