Featured image of post RESTful API

RESTful API

本篇文章主要介绍REST的概念及其API的设计规范

本规范在 API 设计上遵循 REST 架构风格,本部分会针对如何实现 RESTful API,作出说明

REST 介绍

REST涉及一些概念性的东西可能比较多,在实战 RESTful API 之前,要对REST相关的知识有个系统的认知。

诞生

REST(英文:Representational State Transfer,简称REST,直译过来表现层状态转换)是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

它首次出现在 2000 年 Roy Thomas Fielding 的博士论文中,这篇论文定义并详细介绍了表述性状态转移(Representational State Transfer,REST)的架构风格,并且描述了 如何使用 REST 来指导现代 Web 架构的设计和开发。用他自己的原话说:

我写这篇文章的目的是:在符合架构原理前提下,理解和评估基于网络的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。

需要注意的是REST并没有一个明确的标准,而更像是一种设计的风格,满足这种设计风格的程序或接口我们称之为RESTful(从单词字面来看就是一个形容词)。所以RESTful API 就是满足REST架构风格的接口。

意义

既然知道REST和RESTful的联系和区别,现在就要开始好好了解RESTful的一些约束条件和规则,RESTful是一种风格而不是标准。

要理解 RESTful 架构,最好的方法就是去理解 Representational State Transfer 这个词组到底是什么意思,它的每一个词代表了什么涵义。如果把这个名称搞懂了,也就不难体会 REST 是一种什么样的设计。

资源 (Resources) REST 的名称 “表现层状态转化” 中,省略了主语。“表现层” 其实指的是 “资源”(Resources)的 “表现层”。

所谓 “资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个 URI(统一资源定位符)指向它,每种资源对应一个特定的 URI 。要获取这个资源,访问它的 URI 就可以,因此 URI 就成了每一个资源的地址或独一无二的识别符。所谓 “上网”,就是与互联网上一系列的 “资源” 互动,调用它的 URI 。

表现层(Representation) “资源” 是一种信息实体,它可以有多种外在表现形式。我们把 “资源” 具体呈现出来的形式,叫做它的 “表现层”(Representation)。 比如,文本可以用 txt 格式表现,也可以用 HTML 格式、 XML 格式、JSON 格式表现,甚至可以采用二进制格式;图片可以用 JPG 格式表现,也可以用 PNG 格式表现。 URI 只代表资源的实体,不代表它的形式。严格地说,有些网址最后的 “.html” 后缀名是不必要的,因为这个后缀名表示格式,属于 “表现层” 范畴,而 URI 应该只代表 “资源” 的位置。它的具体表现形式,应该在 HTTP 请求的头信息中用 Accept 和 Content-Type 字段指定,这两个字段才是对 “表现层” 的描述。

状态转化(State Transfer) 访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。 互联网通信协议 HTTP 协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生 “状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是 “表现层状态转化”。 客户端用到的手段,只能是 HTTP 协议。具体来说,就是 HTTP 协议里面,四个表示操作方式的动词:GET 、 POST 、 PUT 、 DELETE 。 它们分别对应四种基本操作: GET 用来获取资源, POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。

综述

总结一下什么是 RESTful 架构:

  • 每一个 URI 代表一种资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过四个 HTTP 动词,对服务器端资源进行操作,实现 “表现层状态转化”。

架构特征

从请求的流程来看,RESTful API和传统API大致架构如下:

URI指向资源

URI = Universal Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。URI包括URL和URN,在这里更多时候可能代指URL(统一资源定位符)。RESTful是面向资源的,每种资源可能由一个或多个URI对应,但一个URI只指向一种资源。

无状态

服务器不能保存客户端的信息, 每一次从客户端发送的请求中,要包含所有必须的状态信息,会话信息由客户端保存, 服务器端根据这些状态信息来处理请求。 当客户端可以切换到一个新状态的时候发送请求信息, 当一个或者多个请求被发送之后, 客户端就处于一个状态变迁过程中。 每一个应用的状态描述可以被客户端用来初始化下一次的状态变迁。

REST架构限制条件

Fielding在论文中提出REST架构的6个限制条件,也可称为RESTful 6大原则, 标准的REST约束应满足以下6个原则:

▐ 客户端-服务端(Client-Server):

这个更专注客户端和服务端的分离,服务端独立可更好服务于前端、安卓、IOS等客户端设备。

无状态(Stateless)

服务端不保存客户端状态,客户端保存状态信息每次请求携带状态信息。

▐ 可缓存性(Cacheability)

服务端需回复是否可以缓存以让客户端甄别是否缓存提高效率。

统一接口(Uniform Interface)

通过一定原则设计接口降低耦合,简化系统架构,这是RESTful设计的基本出发点。当然这个内容除了上述特点提到部分具体内容比较多详细了解可以参考这篇 REST论文内容

分层系统(Layered System)

客户端无法直接知道连接的到终端还是中间设备,分层允许你灵活的部署服务端项目。

按需代码(Code-On-Demand,可选)

按需代码允许我们灵活的发送一些看似特殊的代码给客户端例如JavaScript代码。

REST架构的一些风格和限制条件就先介绍到这里,后面就对RESTful风格API具体介绍。

设计规范

整个RESTful API设计主要分为两个大的部分:

描述资源(resource)的URI 和 对资源进行行操作的方法(method)

在这里,分为三个主要部分来讲:

  • 对于资源的URI(Uniform Resource Identifier)设计实践
  • 对于方法的设计实践,这里,我把最常用的方法(GET、POST、PUT、和DELETE)讲一下
  • 对于一些比较特殊的场景,比如,设计支持复杂query/search能力的RESTful API,支持异步处理的RESTful API等,进行一个case by case的说明。

URI设计

在RESTful API设计中,最主要的目的其实是让整个接口更具有自描述性,这样在接口的易用性上和可维护性上才能有更好的表现。

关于资源的URI,简单来讲就是用来在web环境下如何定位一个资源的描述标准

URL为统一资源定位器 ,接口属于服务端资源,首先要通过URL这个定位到资源才能去访问,而通常一个完整的URL组成由以下几个部分构成:

1
URI = scheme "://" host  ":"  port "/" path [ "?" query ][ "#" fragment ]
  • scheme: 指底层用的协议,如http、https、ftp

    如果能全站 HTTPS 当然是最好的,不能的话也请尽量将登录、注册等涉及密码的接口使用 HTTPS。

  • host:服务器的IP地址或者域名

  • port: 端口,http默认为80端口

  • domain:域名,等同于 host + port

    应该尽量将API部署在专用域名之下。

    1
    
    https://api.example.com
    

    如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

    1
    
    https://example.org/api/
    
  • path:访问资源的路径,就是各种web 框架中定义的route路由

  • query: 查询字符串,为发送给服务器的参数,在这里更多发送数据分页、排序等参数。

  • fragment:锚点,定位到页面的资源

我们在设计API时URL的path是需要认真考虑的,而RESTful对path的设计做了一些规范,通常一个RESTful API的path组成如下:

1
/{version}/{resources}/{resource_id}
  • version:API版本号,有些版本号放置在头信息中也可以,通过控制版本号有利于应用迭代。

    应该将API的版本号放入URL。

    1
    
    https://api.example.com/v1/
    

    另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。

  • resources:资源,RESTful API推荐用小写英文单词的复数形式。

  • resource_id:资源的id,访问或操作该资源。

当然,有时候可能资源级别较大,其下还可细分很多子资源也可以灵活设计URL的path,例如:

1
/{version}/{resources}/{resource_id}/{subresources}/{subresource_id}

从大体样式了解URL路径组成之后,对于RESTful API的URL具体设计的规范如下:

  1. 不用大写字母,所有单词使用英文且小写。
  2. 正确使用 "/"表示层级关系,URL的层级不要过深,并且越靠前的层级应该相对越稳定
  3. 结尾不要包含正斜杠分隔符"/"
  4. URL中不出现动词,用请求方式表示动作
  5. 资源表示用复数不要用单数
  6. 不要使用文件扩展名

RESTful API的资源URI一般由两个部分组成的:Path和Query Parameters,下面我来分别介绍和说明一下。

Path

主要是描述一个资源的访问路径的,而Path的一般要用名词来组成。

在RESTful API设计中Path内容还包含有Path Parameter,也就是动态的参数部分,比如,我们如果有一个Path来指向一个具体的公司资源对象,我们会有如下的Path设计

1
http://www.goodhr.com/api/v1/companies/{公司id}

这里{公司id}就是Path parameter,他可以被替换为具体的id值用来表明,那个一个公司资源对象。Path Parameter在整个RESTful API的URI设计理念里面是代表的必选参数,也就是说,Path Parameter的值一定要给的,否则这个RESTful API将无法使用,或者会访问到另外一个API的情况。

其实,在面对不同的场景下,Path的设计上是有不同的实践方式的,下面我把常用的列举出来:

  • 资源对象集

如果用来描述一种资源(一个资源的聚合),那么需要用复数形式表示,比如下面的例子:

1
http://www.goodhr.com/api/v1/companies/66/employees
  • 单独资源对象

如果用来描述一个资源,那么,这个资源肯定是可以有一个唯一标示的来确定这个资源,比如下面的例子:

1
http://www.goodhr.com/api/v1/companies/66/employees/{员工id}
  • 资源从属关系

从属资源关系,比如,员工会从属于一个公司,那么这种uri的设计就应该用下面的方式表达:

1
http://www.goodhr.com/api/v1/companies/66/employees/1001

这种方式的表达另外一个作用就是代表了其资源的生命周期的强依赖,简单来讲如果一个公司被删除了,那么其下面的员工也应该不能再被访问或者修改。

  • 资源索引关系

可索引关系的资源,比如,一个员工可以在一个公司里面加入多个部门,那么,部门和员工的关系并不是一个强从属关系,只是说,通过一个部门可以反查出这个部门里面有哪些员工。

1
http://www.goodhr.com/api/v1/companies/66/departments/{部门id}/employees

上面这种表达方式,其实和从属关系的资源uri没有太大的不同,只是通过增加了departments一级的路径来描述是某一个部门里面的一群员工。

Query Parameters

Query parameter是说在URI中问号(?)之后出现的key-value的参数,

RSETful API 的设计中其实提供的是一个可选参数的作用 — — 不会出现无访问其他资源的情况,并且,也不会造成这个API不能被执行。

RESTful 的标准中,PUT 和 PATCH 都可以用于修改操作,它们的区别是 PUT 需要提交整个对象,而 PATCH 只需要提交修改的信息。

另一个问题是在 POST 创建对象时,究竟该用表单提交更好些还是用 JSON 提交更好些。其实两者都可以,在我看来它们唯一的区别是 JSON 可以比较方便的表示更为复杂的结构(有嵌套对象)。另外无论使用哪种,请保持统一,不要两者混用。

还有一个建议是最好将过滤、分页和排序的相关信息全权交给客户端,包括过滤条件、页数或是游标、每页的数量、排序方式、升降序等,这样可以使 API 更加灵活。但是对于过滤条件、排序方式等,不需要支持所有方式,只需要支持目前用得上的和以后可能会用上的方式即可,并通过字符串枚举解析,这样可见性要更好些。例如:

搜索,客户端只提供关键词,具体搜索的字段,和搜索方式(前缀、全文、精确)由服务端决定:

1
/users/?query=ScienJus

过滤,只需要对已有的情况进行支持:

1
/users/?gender=1

分页:

1
/users/?page=2&per_page=20

比如,我们需要获取一个公司下的所有员工资源对象,那么,我们的API可以设计如下:

1
http://www.goodhr.com/api/v1/companies/66/employees?page=1&size=100

那么上面出现的 pagesize 其实就是 query parameter,用来作为翻页的参数,

如果,在实现这个 RESTful API 的时候,一定要对于当缺少 pagesize 的时候,也会有一个缺省的逻辑实现,在这里,我们一般会说在 page 不给的时候,我们默认是从1开始,如果 size 不给与的时候,我们用缺省的页面大小,如:20来代替。

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。

比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

规范

面向使用者建模

资源不是数据模型, 也不是领域模型,它的语义应该面向使用者。

反例:

1
2
3
4
5
# 面向数据模型设计资源,需要多次请求
/customers/123
/customers/123/baseinfo
/customers/123/tags
复制代码

正例:

1
2
3
# 面向使用者设计,可以把资源定义为:顾客档案
/customers_archives/123
复制代码

资源与角色相关

不同角色的资源可以不同,不同角色使用的资源可以是不一样的,比如:

管理员访问某个顾客的订单:

1
2
GET /customers/123/podcasts
复制代码

顾客访问自己的订单:

1
2
GET /my_podcasts
复制代码

一类资源两个 URL

每个资源都应该只有两个基础 URL(Endpoint),一个 URL 用于集合,另一个用于集合中的某个特定元素。

1
2
3
/customers      # customer 集合
/customers/1    # customer 集合中的特定元素
复制代码

使用一致的复数名词

避免混用复数和单数形式,只应该使用统一的复数名词来表达资源。

反例:

1
2
3
GET /story
GET /story/1
复制代码

正例:

1
2
3
GET /stories
GET /stories/1 
复制代码

复杂的查询逻辑使用查询字符串

保持URL简单短小,将复杂或可选参数移动到查询字符串。

1
2
GET /customers?country=usa&state=ca&city=sfo
复制代码

表达资源之间的关联

当需要对关联在资源1下的资源2进行操作时,使用该形式构造URL:

resources/:resource_id/sub_resources/:sub_resource_id

反例:

1
2
3
GET /cusomters/podcasts/123
GET /getCustomerPodcasts?customer_id=123
复制代码

正例:

1
2
3
GET /cusomters/5678/podcasts        # 获取某个客户的所有播客
GET /cusomters/5678/podcasts/123    # 获取某个客户的某个播客
POST /cusomters/5678/podcasts       # 为某个客户创建一个新播客

方法设计

在RESTful API中,不同的HTTP请求方法有各自的含义,

这里就展示GET,POST,PUT,DELETE几种请求API的设计与含义分析。针对不同操作,具体的含义如下:

1
2
3
4
5
GET /collection:从服务器查询资源的列表(数组)
GET /collection/resource:从服务器查询单个资源
POST /collection:在服务器创建新的资源
PUT /collection/resource:更新服务器资源
DELETE /collection/resource:从服务器删除资源

在非RESTful风格的API中,我们通常使用GET请求和POST请求完成增删改查以及其他操作,查询和删除一般使用GET方式请求,更新和插入一般使用POST请求。从请求方式上无法知道API具体是干嘛的,所有在URL上都会有操作的动词来表示API进行的动作,例如:query,add,update,delete等等。

而RESTful风格的API则要求在URL上都以名词的方式出现,从几种请求方式上就可以看出想要进行的操作,这点与非RESTful风格的API形成鲜明对比。

常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

  • GET /zoos:列出所有动物园
  • POST /zoos:新建一个动物园
  • GET /zoos/ID:获取某个指定动物园的信息
  • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  • DELETE /zoos/ID:删除某个动物园
  • GET /zoos/ID/animals:列出某个指定动物园的所有动物
  • DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

在谈及GET,POST,PUT,DELETE的时候,就必须提一下接口的安全性和幂等性,其中安全性是指方法不会修改资源状态,即读的为安全的,写的操作为非安全的。而幂等性的意思是操作一次和操作多次的最终效果相同,客户端重复调用也只返回同一个结果。

上述四个HTTP请求方法的安全性和幂等性如下:

HTTP Method 安全性 幂等性 解释
GET 安全 幂等 读操作安全,查询一次多次结果一致
POST 非安全 非幂等 写操作非安全,每多插入一次都会出现新结果
PUT 非安全 幂等 写操作非安全,一次和多次更新结果一致
DELETE 非安全 幂等 写操作非安全,一次和多次删除结果一致

GET 方法

这个方法的使用场景很容易理解,就是获取资源,但是,在实际的实践过程中我发现,GET方法所面对的场景反而是最复杂的。比如,复杂的多条件搜索。

  • 获取单独资源对象

获取一个指定的资源对象,比如,通过给定员工id获取一个员工

1
http://www.goodhr.com/api/v1/companies/66/employees/1001

返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
    "companyId": 66,
    "id": 1001,
    "firstname": "Steve",
    "lastname": "Bill",
    "birthDate": "1982-01-01",
    "gender": "male",    
    "hiredDate": "2019-01-01",
    "socialSecurityNumberMask": "123************789",
    "creationTime": "2019-01-01 09:00:01",
    "updatedTime": "2019-01-01 09:00:01"
}
  • 获取资源对象集

比如,获取一个公司下的所有员工

1
http://www.goodhr.com/api/v1/companies/66/employees?pageStart=1&pageSize=100&orderBy=creationTime&order=DESC

对于一个资源集的返回数据设计上,要尽量用加一层字段来进行更好地表现一组数据以及展示是通过什么条件获得的这组数据,设计可以考虑如下方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{    
    "queryConditions": {},
    "pagination": {
        "pageStart": 1,
        "pageSize": 100,
        "sorting": {
            "orderBy": "creationTime",
            "order": "DESC"
        }
    },
    "data": {
        "size": 100,
        "records": [
            {
                "companyId": 66,
                "id": 1001,
                "firstname": "Steve",
                "lastname": "Bill",
                "birthDate": "1982-01-01",
                "gender": "male",                
                "hiredDate": "2019-01-01",
                "socialSecurityNumberMask": "123************789",
                "creationTime": "2019-01-01 09:00:01",
                "updatedTime": "2019-01-01 09:00:01"
            }
            ...
        ] 
    }   
}

在对于一组资源对象进行获取操作的时候,尽量在返回对象中包含获取时候所使用的参数信息,比如,上面,包含了queryConditions用来表示资源获取的时候搜索条件,这里没有,所以是空,pagination用来说明data里面的资源的分页信息,而data字段中不只是包含了返回的资源集合records还包含了,其实际records的大小,以增加这个接口的易用性。

  • 通过搜索获取资源集对象

提供对资源的搜索能力其实在很多系统中都是常见的能力,比如,通过搜索名称获得员工名称,或者是多种条件的组合搜索,比如,姓名加年龄等等。这种RESTful API在设计上,如果用GET方法的话,最大的问题在于GET本身不可以提供request body,所以,一般都会以Query Parameter的方式进行支持。

比如,我们要搜索firstname是steve的,年龄小于60岁的员工,并且,以入职时间倒序来进行返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
http://www.goodhr.com/api/v1/companies/66/employees?firstname=Steve&age=%3C60
pageStart=1&pageSize=100&orderBy=hiredDate&order=DESC{    
    "queryConditions": {
        "firstname":"steve",
        "age": "<60"
    },
    "pagination": {
        "pageStart": 1,
        "pageSize": 100,
        "sorting": {
            "orderBy": "creationTime",
            "order": "DESC"
        }
    },
    "data": {
        "size": 13,
        "records": [
            {
                "companyId": 66,
                "id": 1001,
                "firstname": "Steve",
                "lastname": "Bill",
                "birthDate": "1982-01-01",
                "gender": "male",                
                "hiredDate": "2019-01-01",
                "socialSecurityNumberMask": "123************789",
                "creationTime": "2019-01-01 09:00:01",
                "updatedTime": "2019-01-01 09:00:01"
            }
            ...
        ] 
    }   
}

很多人其实很害怕使用GET来做复杂搜索的话,因为URL的长度限制会有问题,不过,根据http的标准规定2048个字符以内都是没有问题的,所以,一般系统提供的对于资源的搜索能力应该是足够满足了。当然,我们不可否认其实,还是有一些复杂的搜索场景出现的,如果,是这样,可以看后面的三部分的关于特殊场景的一些设计实践介绍。

POST 方法

POST方法代表对uri所描述的资源进行创建一个新的资源。POST方法是可以携带请求体的(request body),RESTful API的body一般使用的是JSON类型。而POST方法的返回类型一般也是JSON格式,并且,HTTP的status code应该是201(Created)

定义一个POST RESTful API一般有两种场景:

  • 资源Id不确定

如果,你要创建的资源的Id是由Server来分发的,那么一般,URI应该是按照资源集的设计风格。比如,创建一个新的职员,其URI如下

1
http://www.goodhr.com/api/v1/companies/66/employees

请求体

1
2
3
4
5
6
7
8
{
    "firstname": "Steve",
    "lastname": "Bill",
    "birthDate": "1982-01-01",
    "gender": "male",
    "hiredDate": "2019-01-01",  
    "socialSecurityNumber": "1234567890123456789"
}

返回结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
    "companyId": 66,
    "id": 1001,
    "firstname": "Steve",
    "lastname": "Bill",
    "birthDate": "1982-01-01",
    "gender": "male",    
    "hiredDate": "2019-01-01",
    "socialSecurityNumberMask": "123************789",
    "creationTime": "2019-01-01 09:00:01",
    "updatedTime": "2019-01-01 09:00:01"
}

这里“id”字段的值是由server端在创建一个employee记录的时候分发的。

  • 资源Id可确定

有一种情况是,你对于要创建的资源的Id是预先确定的,而不需要创建的时候由Server来分发。那么这种情况下,URI应该是按照单独资源的设计风格。

比如,创建一个新的部门(department),其URI如下

1
http://www.goodhr.com/api/v1/companies/66/departments/dept-acct

请求体

1
2
3
4
{
    "name": "财会",
    "introduction": "公司的财务部门"
}

返回结果

1
2
3
4
5
6
7
{
    "companyId": 66,
    "code": "dept-acct",
    "name": "财会",
    "introduction": "公司的财务部门"
    "creationTime": "2019-01-01 09:00:01"
}

PUT 方法

简单来讲PUT方法一般是用来更新一个资源相关信息项目的,根据http的规范定义,PUT应该是幂等的操作,所以,在PUT的接口设计上一定是对资源的信息进行全量的更新,而不是部分更新。这里我们用员工做例子:

URI应该是按照单独资源的设计风格,而返回的http的status code在成功的情况下应该是200

  • 资源对象属性相对简单

一般来讲,一些资源对象所包含的属性信息其实相对简单,那么,这种情况,就用标准的PUT的RESTful API来设计就可以了,比如,员工信息更新:

1
http://www.goodhr.com/api/v1/companies/66/employees/1001

而请求体的设计在这里就要对全量的员工信息进行更新(如下),而不是部分更新 请求体

1
2
3
4
5
6
7
8
{
    "firstname": "Kevin",
    "lastname": "Bill",
    "birthDate": "1982-01-01",
    "gender": "male",    
    "hiredDate": "2018-03-01",  
    "socialSecurityNumber": "1234567890123456789"
}

返回结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
    "companyId": 66,
    "id": 1001,
    "firstname": "Kevin",
    "lastname": "Bill",
    "birthDate": "1982-01-01",
    "gender": "male",    
    "hiredDate": "2018-03-01",
    "socialSecurityNumberMask": "123************789",
    "creationTime": "2019-01-01 09:00:01",
    "updatedTime": "2019-01-05 14:56:12"
}
  • 资源对象有很多属性

有些时候,我们会出现一种场景就是,一个资源所包含的属性很多,而且,这些属性是有一些类型上的归类的,比如,公司资源对象上,会有基本信息,税务信息,董事会信息,营业状态等等,那么在这种情况下,我们如果用一个RESTful API来做全量的更新的话,其实,不论是从这个API的易于使用上,还是以后如果对能访问资源的权限管理上都有一定的问题。这种情况下,我会这么设计这个API,对于不同类型的属性给独立的URI来做,如下:

公司基本信息

1
http://www.goodhr.com/api/v1/companies/66/profile

公司税负信息

1
http://www.goodhr.com/api/v1/companies/66/tax-info

公司董事会信息

1
http://www.goodhr.com/api/v1/companies/66/executive-info

公司营业状态

1
http://www.goodhr.com/api/v1/companies/66/status

DELETE 方法

这个方法顾名思义,就是删除资源,此方法本身也是幂等的,所以,这种接口的实现规则就是,不论是第一次删除一个存在的资源,还是之后删除已经不存在的资源,都应该是返回同样的结果 — — http的status code = 204。

  • 删除单独资源

URI应该是按照单独资源的设计风格,不需要给请求消息体比如,我们删除一个员工。

1
http://www.goodhr.com/api/v1/companies/66/employees/1001

返回消息有两种方式:1. 返回一个空内容,只是http status code为204即可。2. 可以返回一个结果,其内容可以包含被删除员工的关键Id,比如,上面的URI中的公司Id和1001。

1
2
3
4
{
    "companyId": 66,
    "employeeId": 1001
}
  • 删除一批资源

这种情况其实不常出现,但是,是有可能有应用场景的,比如,删除所有离职时间超过3年以上的员工记录,这种URI可以用下面的设计风格:

1
http://www.goodhr.com/api/v1/companies/66/employees/_resigned-exceed-years/3

”_resigned-exceed-years”在这里是一个对于employees资源集的一个刷选条件,而其后面的3就是这个刷选条件的值。

这里我建议要给一个返回值,主要的作用是用来对于这次删除的一个结果报告,如下:

1
2
3
4
{
    "companyId": 66,
    "numberOfDeletedEmployees": 132
}
规范

URL 中不应该包含动词,而是全部使用 Method 来表示动作。

反例:

1
2
3
4
5
6
GET /getCusomters
GET /getAllMaleCusomters
POST /createCusomter
POST /updateCustomer
POST /customer/create_for_management/
复制代码

正例:

1
2
3
4
5
6
7
GET /customers                # 获取客户列表
GET /cusomters?gender=male    # 获取客户列表(过滤出男性)
GET /customers/5              # 获取ID为5的客户
POST /cusomters               # 创建新客户             
PUT /cusomters/5              # 更新已存在的客户5(全量字段)
PATCH /cusomters/5            # 更新已存在的客户5(部分字段)
DELETE /cusomters/5           # 删除客户12

特殊场景

基于2/8理论来说,上面的URI和方法两部分中的设计实践整理你可以理解为覆盖了80%的场景,而这部分,我更多关注的是这个20%的复杂场景。

超复杂搜索

在不常见的场景中,相对常见的是这种过于复杂的搜索RESTful API的提供。这种情况下如果按照标准的GET+query paramters的方式来进行设计,其实,会有出现超出url标准长度要求的可能性,所以,特殊情况,我们就只能特殊处理,一般来说,我们会使用POST方法来代替,然后把搜索条件放到request body里以json的方式提供。

比如,我们需要对一个员工的请假记录进行搜索:

1
http://www.goodhr.com/api/v1/companies/66/timeoff-records?pageStart=1&pageSize=100&orderBy=creationTime&order=DESC

使用POST方法来发送搜索条件信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{    
    "type": [
        "vocation",
        "sick"
    ],
    "appliedDateRange": {
        "startDate": "2019-01-01",
        "endDate": null
    },
    "departments":[
        "dept-acct",
        "dept-tech",
        "dept-marketing"
    ]   
}

返回对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{    
    "queryConditions": {    
        "type": [
            "vocation",
            "sick"
        ],
        "appliedDateRange": {
            "startDate": "2019-06-01",
            "endDate": null
        },
        "departments":[
            "dept-acct",
            "dept-tech",
            "dept-marketing"
        ]   
    },
    "pagination": {
        "pageStart": 1,
        "pageSize": 100,
        "sorting": {
            "orderBy": "creationTime",
            "order": "DESC"
        }
    },
    "data": {
        "size": 13,
        "records": [
            {
                "companyId": 66,
                "id": 10293,
                "applicantId": 1002,
                "applicationDateTime": "2019-01-01 09:00:01",
                "approverId": 98,
                "type": "vocation",
                "timeoffBegin": "2019-06-01 AM",
                "timeoffEnd": "2019-06-01 PM",
                "creationTime": "2019-01-01 09:00:01",
                "updatedTime": "2019-01-01 09:00:01"
            }
            ...
        ] 
    }   
}

异步处理

如果是需要提供比如,备份数据,产生报告结果,复杂的计算任务,或者是需要人工处理的,一般会用异步处理方式,在RESTful API的设计中,所谓的异步和系统中的异步API不太一样在于,他并不是一个基于线程的异步方案,而更多的是,通过一个RESTful API出发请求处理,而这个请求的处理之后的结果则是需要通过另一个API来进行获取。这种异步处理的设计有两种方式,我在下面分别介绍一下。

  • 执行和获取分离

简单的来讲这种设计风格就是,你需要触发API A来创建一个处理请求任务,再通过API B进行轮训获得结果。比如,我们要生成,所有员工的请加统计报告:

第一个RESTful API,我们用来创建生成报告,这里我们用POST方法,因为,每调用一次这个接口,就会创建一个新的报告生成任务

1
http://www.goodhr.com/api/v1/companies/66/statistics/timeoff

请求体

1
2
3
4
5
6
7
{    
    "contentType": "excel",
    "dateRange": {
        "startDate": "2018-01-01",
        "endDate": "2018-12-31",
    }
}

返回对象

1
2
3
4
5
6
7
8
9
{  
    "jobId": "timeoff-100202",
    "creationTime": "2019-01-01 09:00:01",
    "contentType": "excel",
    "dateRange": {
        "startDate": "2018-01-01",
        "endDate": "2018-12-31",
    }
}

这里,jobId就是用来从第二个API来获得这个异步处理任务的结果的。

第二个RESTful API用来获取这个统计报告的,这里我们使用GET方法。

1
http://www.goodhr.com/api/v1/companies/66/statistics/timeoff/{jobId}

返回对象

1
2
3
4
5
6
{  
    "jobId": "timeoff-100202",
    "creationTime": "2019-01-01 09:00:01",
    "status": "finished",
    "retrivalLocation": "https://static.goodhr.com/reports/timeoff-100202.xls"
}

当然,也有可能出现报告还在生成中的情况,那样的话,返回对象中的status有可能就是“processing”,而retrievalLocaiton就是null。

  • 执行完了主动推送

这个设计方案和a对比来说就是,a方案是pull的方式获取结果,而这个方案是push的方式。如果用这个方式设计上面产生timeoff的报告的RESTful API的话,在整体设计上会有一些复杂度,当然,好处就是push的设计方式,系统所受到的系统压力会小很多。

在这个设计方案里,我们只需要提供a里面的第一个POST的RESTful API来做同样的产生报告的任务生成,请求体也没有变化,返回对象中需要增加一个字段”callbackUrl,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{  
    "jobId": "timeoff-100202",
    "creationTime": "2019-01-01 09:00:01",
    "callbackUrl": "https://another-system.somewhere.com/receive-reports/timeoff",
    "contentType": "excel",
    "dateRange": {
        "startDate": "2018-01-01",
        "endDate": "2018-12-31",
    }
}

这里的callbackUrl其实是一个webhook设计,对于webhook,如果是内部系统,建议简单的加一个host白名单校验,而如果是以一种SaaS的Open API的方式向第三方提供扩展能力,那么就需要额外的搭建一个webhook的管理功能来保障足够的安全性,比如跨域攻击或请求拦截等。

超多字段资源对象

有些时候,一些资源对象包含了很多属性,几十个,甚至上百,而这个时候,我们通过一个RESTful API来获取这个资源,如果,只是通过Id获取确定的一个资源对象,那其实还是可以接受的,但是,如果是获取资源对象集,那么不论是系统开销还是网络开销,都会产生不必要的浪费。

对于这种资源的相关RESTful API的设计上可以考虑在其URI的设计上上提供一个query parameter作为告诉服务端这个资源的那些属性需要返回,比如,对于公司资源的访问上。

1
http://www.goodhr.com/api/v1/companies/66?fields={属性类型}

这里fields的值可以按照一个公司对象的属性分类进行提供,比如,添加tax-info代表需要返回税务信息,添加executive-info代表需要返回管理层信息。当然,如果fields没有给的话,我们应该有一个缺省的返回内容逻辑,比如,返回公司基本信息作为缺省的返回属性。

这种设计方案中fields的力度最好是一种属性,而不是一个属性,这样在代码的可读性上,以及服务端的代码实现复杂度和灵活性上会有一个相对不错的平衡,假设,如果你需要这个接口的调用者把具体的每一个属性名称都要给的话,那么,这将是一个灾难。

幂等操作设计

这种RESTful API的设计其实很简单,一般都是在URI上提供一个query parameter用来代表一个事务id,一次可以让你实现不断的对次事务进行安全的重试操作。

假设,我们的系统对于创建一个员工记录的逻辑中,包含了和其他系统的交互以及数据的更新,那么,我们就需要改造一下之前POST方法中创建员工记录的例子,让其可支持幂等操作。

1
http://www.goodhr.com/api/v1/companies/66/employees

请求体

1
2
3
4
5
6
7
8
{
    "firstname": "Steve",
    "lastname": "Bill", 
    "birthDate": "1982-01-01",
    "gender": "male",
    "hiredDate": "2019-01-01",  
    "socialSecurityNumber": "1234567890123456789"
}

这里,我们对于返回结果增加一个字段transactionId,用来表示本次创建请求的事务。

返回结果

1
2
3
4
{
    "transactionId": "e721ac103ckc910ck20c",
    ...
}

如果,创建过程中出现异常(非204 status code),造成你的创建员工记录失败,那么,你可以在第二次重试的时候把这个transactionId加上,以保证服务端可以实现一个幂等操作,如下:

1
http://www.goodhr.com/api/v1/companies/66/employees?transactionId=e721ac103ckc910ck20c

返回值

服务端处理完成后客户端也可能不知道具体成功了还是失败了,服务器响应时,包含状态码返回数据两个部分。

状态码

我们首先要正确使用各类状态码来表示该请求的处理执行结果。状态码主要分为五大类:

1xx:相关信息 2xx:操作成功 3xx:重定向 4xx:客户端错误 5xx:服务器错误

每一大类有若干小类,状态码的种类比较多,而主要常用状态码罗列在下面:

200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) 204 NO CONTENT - [DELETE]:用户删除数据成功。 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

详细介绍

在使用 HTTP Status Code 的基础上,还需要有业务错误码,通过code字段返回。错误码由各业务方自行约定,业务内部自行划分区段。

返回数据(json)

针对不同操作,服务器向用户返回数据,而各个团队或公司封装的返回实体类也不同,但都返回JSON格式数据给客户端。

使用相同的 HTTP 响应结构,推荐使用下列结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{

  "code": 0,            # 错误码,请求成功时返回0
  "msg": "success",     # 错误信息,请求成功时返回"success"
  "ok": true,           # 返回状态是否成功
  "data": {             # 数据内容,结构必须为object,使用 list/string 均不合规范
    "id": 1,
    "name": "abc"
  },
  "extra": {            # 错误码非0时,data应为空,推荐extra字段返回错误时需要携带的信息
  
  }
}

结语

这个原则主要强调的是,每一个接口都要可以完成一整个业务逻辑,而不是通过调用方组合多个接口完成一个业务逻辑的。

比如,创建一个员工记录的时候,需要调用政府的一个系统接口对其SIN(社会保险号)进行校验,那么最佳的实现方式是,要有一个创建员工记录接口一次性完成所有的SIN校验以及记录创建,如下,

good

1
POST: http://www.goodhr.com/api/v1/companies/66/employees

请求体

1
2
3
4
5
6
7
8
{
    "firstname": "Steve",
    "lastname": "Bill",
    "birthDate": "1982-01-01",
    "gender": "male",
    "hiredDate": "2019-01-01",  
    "socialSecurityNumber": "1234567890123456789"
}

相反,有的时候会有人把这两个逻辑拆分为两个独立的接口来提供,就会变成下面的效果

bad

校验社会保险号

1
GET: http://www.goodhr.com/api/v1/sin-record/1234567890123456789

创建员工记录

1
POST: http://www.goodhr.com/api/v1/companies/66/employees

请求体

1
2
3
4
5
6
7
8
9
{
    "firstname": "Steve",
    "lastname": "Bill",
    "birthDate": "1982-01-01",
    "gender": "male",
    "hiredDate": "2019-01-01",  
    "socialSecurityNumber": "1234567890123456789"
    "sinVerfiied": true
}

版本

一定要有版本化管理,这样不仅仅是便于维护,更为了让你的系统更容易地进行升级和重构。

在RESTful API的中,版本的实践方式一般是在uri中Path中添加版本信息

1
http://www.goodhr.com/api/v{版本号}

版本号部分可以是数字,也可以是其他你希望的,比如,有的会使用日期如20190101这种。

拓展资料

GraphQL和REST比较:谁才是最佳API设计架构

GraphQL vs REST API 架构,谁更胜一筹?

GitHub RESTFul API

GitHub GraphQL API

渝ICP备2022001449号
本站总访问量