总方针

构建易于理解和使用的RESTful接口。

接口应该是直观的,调用者可以通过接口来获得系统或应用程序中所有业务服务的工作节奏。

一般来说,可以使用以下的指导方针来进行接口的设计。

  1. 使用标准HTTP动词–围绕这些HTTP动词(GET/PUT/POST/PATCHDELETE)对基本的行为进行建模。
  2. 使用URI来传达意图–使用URI来描述问题域中的不同资源,并为问题域内的资源的关系提供一种基本机制。
  3. 使用JSON进行响应–JSON是一种轻量级的数据序列化协议。
  4. 使用HTTP状态码来传达结果–HTTP协议具有丰富的标准响应代码,来指示服务的成功和失败。学习这些状态码,并且,最重要的是,在所有接口中始终如一地使用它们。

所有这些指导方针都是为了完成一件事,那就是使接口易于理解和使用。 我们希望调用者坐下来查看一下接口就能开始使用它们。 如果接口不容易使用,开发人员就会另辟道路,破坏架构的意图。

资源与URI

REST全称是 Representational State Transfer (表述性状态转移)。其中表述指的就是资源。

URI既可看成是资源的地址,也可以看成是资源的名称。

URI的设计应该遵循可寻址性原则,具有自描述性,需要在形式上给人以直觉的关联。

URIHTTP动词作用的对象。它应该只有名词,不能包含动词。

URI的设计应该注意:

  1. URI中不能有动词: 动词应该由HTTP的动作(GET/POST/PUT/PATCH/DELETE等)来表示
  2. URI结尾不应该包含斜杠“/”
  3. 正斜杠分隔符“/”必须用来指示层级关系
  4. 应该使用连接符“-”来提高URI的可读性:浏览器默认会给超链接加上下划线,因此不要用其做URI分隔符
  5. URI路径首选小写字母:RFC-3986URI定义为区分大小写,但URI中的scheme(协议名)和host(主机名)除外
  6. URI路径中的名词建议使用复数
  7. 避免层级过深的URI(太多的层级在另一个侧面反应该接口有太多的职责)

资源操作

HTTP 通常有以下5种动词:

  • GET:获取资源(幂等)
  • POST:新建资源(非幂等)
  • PUT:更新资源(所有属性)(幂等)
  • PATCH:更新资源(部分属性)(非幂等)
  • DELETE:删除资源(幂等)

根据 HTTP 规范,动词一律大写。

资源过滤

很多情况,资源会有多级分类,因此很容易写出多级的URI,比如某个作者的某一类文章(/authors/123/categories/2)。

这种URI不易于扩展,语义也不明确,不能直观表达其含义。

更好的做法是,将次要的级别用查询字符串进行表达。如:

/authors/123?category=2
/articles?published=true

同样的,通过使用查询字符串,实现排序、投影和分页。

与之相反

经常使用的、复杂的查询可以标签化。 如:

/authors/123?status=close&sort=created,desc

可转化为:

/authors/123/closed
// 或者
/authors/123#closed

返回状态码

HTTP状态码为三位数,分五类:

  • 1** 相关信息
  • 2** 操作成功
  • 3** 重定向相关
  • 4** 客户端(导致的)错误
  • 5** 服务端(导致的)错误

HTTP包含了100多个状态码,覆盖了大多数可能遇到的情况。 每一种状态都有标准的(或约定的)解释,客户端只需查看状态,就可以大致判断发生了什么情况。 所以服务器应该尽可能使用这些标准的HTTP状态码,来表达执行结果状态。

通常不需要1**这一类状态码。 以下是常用的:

  • 200 OK : 成功返回请求数据(幂等)
  • 201 Created : 新建数据成功
  • 202 Accepted : 表示服务器已接收请求,但未处理。通常用于异步操作。
  • 204 No Content : 删除数据成功
  • 301 Moved Permanently : 资源已永久性迁移,需要使用新的(写在相应头Location中的)URI访问。允许客户端把POST请求修改为GET
  • 302 Found : 不推荐使用,此代码在HTTP1.1协议中已被303/307替代。目前对302的使用和最初的HTTP1.0定义的语义是有出入的,应该只有在GET/HEAD方法下,客户端才能根据Location执行自动跳转,而目前的客户端基本上是不会判断原请求方法,无条件的执行重定向。
  • 303 See Other : 参考另一个URI(区别:307用于GET303用于POSTPUTDELETE),但不强制要求重定向。
  • 304 Not Modified : 服务器资源与客户端最近访问的一致,不返回资源消息体。
  • 307 Temporary Redirect : 目前URI不能提供所请求的资源,临时重定向到另外一个URI。用来替代HTTP1.0中的302
  • 308 Permanent Redirect : 与301类似,但客户端不能修改原请求方法
  • 400 Bad Request : 服务器不理解客户端的请求,未做任何处理
  • 401 Unauthorized : 用户未提供身份验证凭据,或没通过验证
  • 403 Forbidden : 用户通过的验证,但不具有访问权限
  • 404 Not Found : 请求资源不存在或不可用。可以对某些用户未授权访问的资源操作返回该状态码,目的是防止私有资源泄露(知道有该资源)。
  • 405 Method Not Allowed : 用户已通过验证,但所用的HTTP方法不在权限内或资源只读等。响应Header中应申明支持的方法
  • 406 Not Acceptable : 表示拒绝处理该请求(如:服务端只能返回JSON,但客户端要求XML
  • 409 Conflict : 资源状态冲突,例如客户端尝试删除一个有约束的资源
  • 410 Gone : 请求资源已从这个地址转移,不再可用
  • 412 Precondition Failed : 用于有条件的操作不能被满足
  • 415 Unsupported Media Type : 请求格式不支持(如:服务端只能返回JSON,但客户端要求XML
  • 422 Unprocessable Entity : 请求无法处理,或发生了一个验证错误
  • 429 Too Many Requests : 请求次数超过限制
  • 500 Internal Server Error : 请求有效,服务器处理时发生内部错误
  • 503 Service Unavailable : 服务器无法处理请求,多半是服务器问题,如CPU高等

返回内容

返回内容数据格式应该是结构化的(如:一个JSON对象)。

客户端请求时也要明确告诉服务器,可以接受的格式。

  • GET /collections 200 返回资源列表
  • GET /collections/:id 200 返回单个资源
  • POST /colections 201 返回新增的资源对象
  • PUT /collections/:id 200 返回完整的资源对象
  • PATCH /collections/:id 200 返回完整的资源对象或被修改的属性
  • DELETE /collections/:id 204 返回空文档

错误处理

错误时不要返回200状态码。 因为只有解析数据体后,才能得知操作失败。而且与HTTP状态码描述冲突。

假如你不利用HTTP状态码丰富的应用语义,那么你就错失了提高重用性、增强互操作性和提升松耦合性的机会。

这些所谓的RESTful应用必须通过响应体才能给出错误信息,那么这个跟SOAP没什么区别。

正确的做法是,状态码反映发生的错误,而具体的错误信息放在数据体中。 如:

HTTP/1.1 400 Bab Request
Content-Type: application/json

{
	"error": "Invalid param."
	"data": {
		"name": "This field is required."
	}
}

另外建议要区分业务异常和非业务异常。 业务异常的返回4**的状态码,非业务异常的返回500状态码。

资源的表述

客户端通过HTTP方法获取的不是资源本身,而是资源的一种表述而已。 资源在外界的具体呈现,可以有多种表述形式,如:html、xml、json、png、jpg等。

资源的表述包括数据和描述数据的元数据,如:HTTP头中的Content-Type就是一个元数据属性。

所以应该通过HTTP的内容协商,来获取资源的表述。

如:客户端可以通过Accept头请求一种特定格式的表述,服务器则通过Content-Type告诉客户端资源的表述形式。

区分格式

应该优先使用内容协商来区分表述格式。

使用后缀表示格式,无疑是直观的,但它混淆了资源的名称和资源的表述形式。

超媒体(Hypermedia)

“超媒体即应用状态引擎(hypermedia as the engine of application state)”。

当浏览Web网页时,我们从一个链接跳到一个页面,再从页面里的另一个链接跳到另一个页面,这就是在用超媒体的概念:把一个个资源链接起来。

要达到这个目的,就要求在资源的表述里加上链接来引导客户端。

如 GitHub api 中的分页,是在头信息的Link提供:

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"

应该多花时间来给资源的表述提供链接,而不是专注于寻找漂亮的URI

速率限制

响应头建议包含当前限流状态

如 GitHub api 中使用3个相关的头信息进行说明:

  • X-RateLimit-Limit: 用户在时间窗口下发送请求的最大值
  • X-RateLimit-Remaining: 当前时间窗口剩下的可用请求数
  • X-RateLimit-Rest: 为了得到最大请求数(或到下一时间窗口)所等待的秒数

建议同时提供可以不影响RateLimit的请求接口,来查询当前的RateLimit

无状态

RESTful应该是无状态通信的。 服务端不应该保存客户端(应用)状态。

客户端与服务端交互必须是无状态的,并在每次请求中包含处理所需的一切信息。

这种无状态通信,使得服务端能够理解独立的请求和响应。 在多次请求中,同一客户端也不再需要依赖同一服务器,方便实现高可扩展和高可用性的服务端。

服务端通过超媒体告诉客户端当前(应用)状态可以有哪些后续的状态。 这些类似“下一页”的链接将指引客户端如何从当前状态进入下一个可能的状态。

版本

三种方式:

  1. URI中:/api/v1/**
  2. Accept Header: Accept: application/json+v1
  3. 自定义Header: X-Api-Version: 1

建议第一种,虽然没那么优雅,但最明显方便。

另一种观点:一个资源,应只有一个单一的URI来标示,资源版本不应该体现在URI中。

以上见仁见智,不强制要求。

  • API 失效:返回404 Not Found410 Gone
  • API 迁移:返回301/303307

其他(Header)头信息的使用

  • Last-Modified : 用于服务器端的响应,是一个资源最后被修改的时间戳,客户端(缓存)可以根据此信息判断是否需要重新获取该资源。
  • ETag : 服务器端资源版本的标识,客户端(缓存)可以根据此信息判断是否需要重新获取该资源,需要注意的是,ETage如果通过服务器随机生产,可能会存在多个主机对同一个资源产生不同的ETag的问题。
  • Location : 如在成功创建了一个资源后,可以把新资源的URL放在Location中;又如,在异步请求时,接口返回响应202(Accepted)的同时,可以给客户端一个查询异步状态的地址。
  • Cache-Control, Expires, Date : 通过缓存机制提升接口响应性能,同时根据实际需要也可以禁止客户端对接口请求做缓存。对应某些实时性要求不高的情况下,可以使用max-age来指定一个小的缓存时间,这样对客户端和服务端都有利。一般来说,只对GET方法且返回200的资源使用缓存,在某些情况下也可以对返回3**4**的情况做缓存,防范错误访问带来的负载。
  • 自定义头 : 不能改变HTTP方法的性质,尽量保持的简单,不要用body中的信息对其补充说明。

其他

1. 动词的覆盖

有些客户端仅支持GETPOST两种方法。那么,服务器必须可以接受通过POST模拟其他方法(PUTPACTHDELETE)。

客户端在发送HTTP请求时,要加上X-HTTP-Method-Override头信息,告诉服务器应该使用那个动词,覆盖POST方法。

2. 提供相关链接

服务接口的使用者未必知道接口有那些,以及它的相关服务。 好的接口,应该在相应中给出相关链接,以便于下一步操作。 这样,用户就可以发现其他接口的URI。 这种方法叫HATEOAS。 如 GitHub 的 API 都在api.github.com这个域名。

参考