术→技巧, 研发

RESTful API设计指南

钱魏Way · · 642 次浏览

什么是RESTful API?

要弄清楚什么是RESTful API,首先要弄清楚什么是REST。REST:全称是 Resource Representational State Transfer,或者说表现层状态转移。看概念,估计没人能明白。用一句人话解释:URL定位资源,用HTTP动词(GET,POST,PUT,DELETE)描述操作。

资源(Resources)

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

  • URI:Uniform Resource Identifier,统一资源标识符
  • URL:Uniform Resource Location,统一资源定位符

所谓”上网”,就是与互联网上一系列的”资源”互动,调用它的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用来删除资源。

REST的基本原则

  • C-S架构:数据的存储在Server端,Client端只需使用就行。两端单独开发,互不干扰
  • 无状态:http请求本身就是无状态的,基于C-S架构,客户端的每一次请求带有充分的信息能够让服务端识别。服务端能够根据请求的各种参数,无需保存客户端的状态,将响应正确返回给客户端。
  • 统一的接口:REST架构的核心内容,统一的接口可以让客户端只需要关注实现接口就可以,接口的可读性加强,使用人员方便调用。REST接口约束定义为:通过uri标出你要操作的资源,通过请求动作(http method)标识要执行的操作,通过返回的状态码来表示这次请求的执行结果。
  • 一致的数据格式:服务端返回的数据格式要么是XML,要么是Json,或者直接返回状态码
  • 可缓存:在万维网上,客户端可以缓存页面的响应内容。因此响应都应隐式或显式的定义为可缓存的,若不可缓存则要避免客户端在多次请求后用旧数据或脏数据来响应。
  • 按需编码、可定制代码:REST允许客户端通过下载并执行一些来自于服务端的脚本程序,来对客户端功能进行扩展。这样可以简化客户端功能的开发,比如常见的移动端webview,web小游戏等。

Restful API成熟度

Richardson Maturity Model模型中,将RESTful分为4个等级:

4个等级分别是:

  • 第一级(Level 0)的 Web 服务仅使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
  • 第二级(Level 1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。
  • 第三级(Level 2)的 Web 服务使用不同的HTTP 方法来进行不同的操作,并且使用HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。
  • 第四级(Level 3)的 Web 服务使用HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。

RESTful API设计要点

URL的构成

协议

提供给用户的API,尽量使用HTTPs协议。使用HTTPs协议还是HTTP协议本身和RESTful API并无关系,但是这对于提高网站的安全性很重要

域名

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

https://api.example.com

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

https://example.org/api/

版本

API实在无法演进,则必须提供不同版本的API。版本控制允许在不破坏客户端的情况下,在新版本中发布不兼容和重大更改的API。

有两种最流行的版本控制方法:

  • 通过URLs版本化
  • 通过Accept HTTP Header进行版本控制(内容协商)

通过URLs版本化

只需将API的版本号放在每个资源的URL中即可。不需要使用次级版本号(“v1.2”),因为你不应该频繁的去发布API版本。

/v1/epics

优点:

  • 对API开发人员非常简单。
  • 对客户端访问也非常简单。
  • 可以复制和粘贴URL。

缺点:

  • 非RESTful。(该方式会令URL发生变化)
  • 破坏URLs。 客户端必须维护和更新URL。

由于其简单性,该方式被各大厂商广泛使用,例如:Facebook, Twitter, Google/YouTube, Bing, Dropbox, Tumblr以及Disqus等。

通过Accept HTTP Header进行版本控制(内容协商)

更RESTFul的方式是利用通过Accept HTTP请求头的内容协商。

GET /epics
Accept: application/vnd.myapi.v2+json

优点:

  • URLs保持不变
  • RESTFul方式
  • HATEOAS友好

缺点:

  • 稍微难以使用。客户必须注意标题。
  • 无法再复制和粘贴网址。

Github采用这种做法。

请谨记一点,API是服务器与客户端之间的一个公共契约。如果你对服务器上的API做了一个更改,并且这些更改无法向后兼容,那么你就打破了这个契约,客户端又会要求你重新支持它。为了避免这样的事情,你既要确保应用程序逐步的演变,又要让客户端满意。那么你必须在引入新版本API的同时保持旧版本API仍然可用。

如果你只是简单的增加一个新的特性到API上,如资源上的一个新属性或者增加一个新的端点,你不需要增加API的版本。因为这些并不会造成向后兼容性的问题,你只需要修改文档即可。随着时间的推移,你可能声明不再支持某些旧版本的API。申明不支持一个特性并不意味着关闭或者破坏它。而是告诉客户端旧版本的API将在某个特定的时间被删除,并且建议他们使用新版本的API。

随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

路径(Endpoint)

路径又称”终点”(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的”集合”(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

  • https://api.example.com/v1/zoos
  • https://api.example.com/v1/animals
  • https://api.example.com/v1/employees

减少路径嵌套

在一些有父路径/子路径嵌套关系的资源数据模块中, 路径可能有非常深的嵌套关系, 例如:

  • /orgs/{org_id}/apps/{app_id}/dynos/{dyno_id}

推荐在根(root)路径下指定资源来限制路径的嵌套深度。使用嵌套指定范围的资源,例如在下面的情况下,dyno属性app范围下,app属性org范围下:

  • /orgs/{org_id}
  • /orgs/{org_id}/apps
  • /apps/{app_id}
  • /apps/{app_id}/dynos
  • /dynos/{dyno_id}

过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,也可以使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3;

URI规范

  • 不用大写
  • 用中杠-不用下杠_
  • 参数列表要encode

使用统一的资源路径

每个资源使用两个URL

资源集合用一个URL,具体某个资源用一个URL:

  • /employees           #资源集合的URL
  • /employees/56    #具体某个资源的URL

用名词代替动词表示资源

这让你的API更简洁,URL数目更少。不要这么设计:

  • /getAllEmployees
  • /getAllExternalEmployees
  • /createEmployee
  • /updateEmployee

更好的设计:

  • GET /employees
  • GET /employees?state=external
  • POST /employees
  • PUT /employees/56

推荐用复数名词

所用的名词往往和数据库的表名对应,而数据库的表是一组记录的集合,因此URL中的名词即表示一组资源的集合,故URI中的名词要使用复数

推荐:

  • /employees
  • /employees/21

不推荐:

  • /employee
  • /employee/21

事实上,这是个人爱好问题,但复数形式更为常见。此外,在资源集合URL上用GET方法,它更直观,特别是GET /employees?state=external、POST /employees、PUT /employees/56。但最重要的是:避免复数和单数名词混合使用,这显得非常混乱且容易出错。

非资源请求用动词

有时API调用并不涉及资源(如计算,翻译或转换)。例:

  • GET /translate?from=de_DE&to=en_US&text=Hallo
  • GET /calculate?para2=23&para2=432

在这种情况下,API响应不会返回任何资源。而是执行一个操作并将结果返回给客户端。因此,您应该在URL中使用动词而不是名词,来清楚的区分资源请求和非资源请求。

异步任务

对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

提交任务:

POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

返回:

{"taskId":3,"createBy":"Anonymous","status":"running"}
GET /task/3

{"taskId":3,"createBy":"Anonymous","status":"success"}

如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

提交任务:

POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

返回:

{"taskId":3,"createBy":"Anonymous"}
GET /task/3/status

{"progress":"50%","total":18,"success":8,"fail":1}

Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

{
    "link": {
        "rel": "collection https://www.example.com/zoos",
        "href": "https://api.example.com/zoos",
        "title": "List of zoos",
        "type": "application/vnd.yourformat+json"
    }
}

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

{
    "current_user_url": "https://api.github.com/user",
    "authorizations_url": "https://api.github.com/authorizations",
    // ...
}

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

{
    "message": "Requires authentication",
    "documentation_url": "https://developer.github.com/v3"
}

上面代码表示,服务器给出了提示信息,以及文档的网址。

正确使用HTTP动词

对于资源的具体操作类型,由HTTP动词表示。

常用的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:删除某个指定动物园的指定动物

2个URL乘以4个HTTP方法就是一组很好的功能。看看这个表格:

POST(创建) GET(读取) PUT(更新) DELETE(删除)
/employees 创建一个新员工 列出所有员工 批量更新员工信息 删除所有员工
/employees/56 (错误) 获取56号员工的信息 更新56号员工的信息 删除56号员工

POST与GET的区别

在网站开发的时候通常会对POST和GET产生混淆,让人混淆的主要原因是基本上POST能解决的问题GET都能解决,反之亦然。

GET:字面理解就是获取资源

  • GET请求标准上是幂等的(用户应该认为请求是安全的-资源不会被修改,这里所以说应该是服务器端并不保证资源不会被修改)
  • GET请求可以被浏览器缓存;响应也可以被缓存(根据缓存头信息来处理)
  • GET请求可以保存在浏览器历史记录中,也可以作为链接分发或分享,可以收藏为书签
  • GET请求的数据都在URL中,可以方便都从浏览器中获取数据(因此不能携带诸如密码的明文数据)
  • GET请求的长度会有限制(比如IE的路径总长度需小于2048个字符)
  • GET请求的数据只能包含ASCII字符

POST:字面理解就是发布新资源

  • POST请求标准上不是幂等的(用户应该认为请求是有副作用的-可能会导致资源修改)
  • POST请求URL可以被浏览器缓存,但是POST数据不会被缓存;响应可以被缓存(根据缓存头信息来处理)
  • POST请求不便于分发或分享,因为POST数据会丢失,不能收藏为书签。
  • POST请求没有长度限制,可以用来处理“请求数据”很大的场景(只要不超过服务器端的处理能力)
  • POST请求的数据不限于ASCII字符,可以包含二进制数据

上面两者区别的解释中幂等可能不太好理解,幂等(idempotent、idempotence)其实是一个数学或计算机学概念,常见于抽象代数中。幂等具体表现为:

  • 对于单目运算,如果一个运算对于在范围内的所有的一个数多次进行该运算所得的结果和进行一次该运算所得的结果是一样的,那么我们就称该运算是幂等的。比如绝对值运算就是一个例子,在实数集中,有abs(a)=abs(abs(a))。
  • 对于双目运算,则要求当参与运算的两个值是等值的情况下,如果满足运算结果与参与运算的两个值相等,则称该运算幂等,如求两个数的最大值的函数,即max(x,x) = x。

通俗的讲幂等的意味着对同一URL的多个请求应该返回同样的结果。但其实也不不是非常的严格,比如新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是和幂等的,因为它总是返回当前的新闻。从根本上说,如果目标是当用户打开一个链接时,他可以确信从自身的角度来看没有改变资源即可。

早期的Web MVC框架设计者们并没有有意识地将URL当作抽象的资源来看待和设计,所以导致一个比较严重的问题是传统的Web MVC框架基本上都只支持GET和POST两种HTTP方法,而不支持PUT和DELETE方法。

允许覆盖HTTP方法

一些代理只支持POST 和 GET方法,为了使用这些有限方法支持RESTful API,需要一种办法覆盖http原来的方法。

使用订制的HTTP头 X-HTTP-Method-Override 来覆盖POST 方法。

使用HTTP状态码

RESTful Web服务应使用合适的HTTP状态码来响应客户端的请求。

  • 2xx – 成功 – 一切正常。
  • 4xx – 客户端错误 – 如果客户端的故障(例如:客户端发送无效请求或未经授权)
  • 5xx – 服务器错误 – 服务端的故障(尝试处理请求时的错误,如数据库故障,依赖服务不可用,编码错误或不应发生的状态)

请注意,Http状态码提供了70多个状态码,使用所有过多的HTTP状态码可能会让API用户感到困惑。所以应该保持使用精简的HTTP状态码集。常用状态码如下:

  • 2xx:成功,操作被成功接收并处理
    • 200:请求成功。一般用于GET与POST请求
    • 201:已创建。成功请求并创建了新的资源
  • 3xx:重定向,需要进一步的操作以完成请求
    • 301:永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
    • 304:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
  • 4xx:客户端错误,请求包含语法错误或无法完成请求
    • 400:客户端请求的语法错误,服务器无法理解
    • 401:请求要求用户的身份认证
    • 403:服务器理解请求客户端的请求,但是拒绝执行此请求
    • 404:服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面
    • 410:客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
  • 5xx:服务器错误,服务器在处理请求的过程中发生了错误
    • 500:服务器内部错误,无法完成请求

不要过度使用404。状态码的使用要尽量精确。如果资源可用,但禁止用户访问,则返回403。如果资源曾经存在但现已被删除或停用,请使用410。

结果过滤,排序和搜索

最好是尽量保持基本资源URL的简洁性。复杂结果过滤器、排序需求和高级搜索 (当限定在单一类型的资源时),都能够作为在基本URL之上的查询参数来轻松实现。

Filtering过滤器

使用唯一的查询参数进行过滤:

  • GET /cars?color=red 返回红色的cars
  • GET /cars?seats<=2 返回小于两座位的cars集合

另外还可以使用JSON API方式过滤:

  • GET /employees?filter[state]=internal&filter[title]=senior
  • GET /employees?filter[id]=1,2

Sorting排序

允许针对多个字段排序

  • GET /cars?sort=-manufactorer,+model

这是返回根据生产者降序和模型升序排列的car集合

Paging分页

两种流行的分页方法是:

  • 基于偏移的分页
  • 基于键集的分页,又称继续令牌,也称为光标(推荐)

基于偏移的分页

一般方法是使用参数offset和limit来进行分页:

  • /epics?offset=30&limit=15  # 返回30至45的epics

如果未填参数,则可使用默认值(offset=0,limit=100 ):

  • /epics # 返回0至100的epics

还可以在响应数据中,提供前一页和后一页的链接:

请求:

  • /epics?offset=30&limit=15 # 返回30至45的epics

响应:

{
    "pagination": {
        "offset": 20,
        "limit": 10,
        "total": 3465,
    },
    "data": [
        //...
    ],
    "links": {
        "next": "http://www.domain.com/epics?offset=30&limit=10",
        "prev": "http://www.domain.com/epics?offset=10&limit=10"
    }
}

为了将总数发给客户端,使用订制的HTTP头: X-Total-Count。

链接到下一页或上一页可以在HTTP头的Link设定,遵循Link设定:

Link: <https://blog.mwaysolutions.com/sample/api/v1/cars?offset=15&limit=5>; rel="next",<https://blog.mwaysolutions.com/sample/api/v1/cars?offset=50&limit=3>; rel="last",<https://blog.mwaysolutions.com/sample/api/v1/cars?offset=0&limit=5>; rel="first",<https://blog.mwaysolutions.com/sample/api/v1/cars?offset=5&limit=5>; rel="prev",

基于偏移量的分页实现很简单,但是有两个缺点:

  • 查询慢。数据量大时SQL偏移子句执行会很慢。
  • 不安全。分页期间的变更。

基于键集的分页,又称继续令牌,也称为光标(推荐)

简单来说就是使用索引列来进行分页。 假设epic有一个索引列data_created,我们就可以使用data_created来分页。

  • GET /epics?pageSize=100 #客户端接受最靠前的100条epic信息,使用data_created字段排序
  • GET /epics?pageSize=100&createdSince=1504224000000 # 该分页最老epic的dataCreated`字段值为 1504224000000 (= Sep 1, 2017 12:00:00 AM),客户端请求1504224000000之后的100个epics数据。

该分页最前面的epic创建于1506816000000。该分页方式解决了基于偏移的分页的许多缺点,但对调用方来说不太方便。

更好的方式是通过向日期添加附加信息(如id)来创建所谓的 continuation token,以提高可靠性和效率。 此外,应该向该令牌的有效负载中提供专用字段,以便客户端不用必须通过查看元素才能搞清楚。甚至还可以进一步提供下一页链接。

因此 GET /epics?pageSize=100请求将返回如下:

{
    "pagination": {
        "continuationToken": "1504224000000_10",
    },
    "data": [
        // ...
        // last element:
        {
            "id": 10,
            "dateCreated": 1504224000000
        }
    ],
    "links": {
        "next": "http://www.domain.com/epics?pageSize=100&continue=1504224000000_10"
    }
}

下一页链接使API真正成为RESTful风格,因为客户端只需通过这些链接(HATEOAS)即可查看集合。 无需手动构建URL。此外,服务端可以简单地更改URL结构而不会破坏客户端,保证接口的演进性。

Field selection

移动端能够显示其中一些字段,它们其实不需要一个资源的所有字段,给API消费者一个选择字段的能力,这会降低网络流量,提高API可用性。

  • GET /cars?fields=manufacturer,model,id,color

Search 搜索

有时基本的过滤不能满足需求,这时你就需要全文检索的力量。或许你已经在使用ElasticSearch或者其它基于Lucene的搜索技术。当全文检索被用作获取某种特定资源的资源实例的机制时, 它可以被暴露在API中,作为资源终端的查询参数,我们叫它“q”。搜索类查询应当被直接交给搜索引擎,并且API的产出物应当具有同样的格式,以一个普通列表作为结果。

把这些组合在一起,我们可以创建以下一些查询:

  • GET /tickets?sort=-updated_at – 获取最近更新的票
  • GET /tickets?state=closed&sort=-updated_at – 获取最近更新并且状态为关闭的票。
  • GET /tickets?q=return&state=open&sort=-priority,created_at – 获取优先级最高、最先创建的、状态为开放的票,并且票上有 ‘return’ 字样。

Bookmarker 快捷方式

  • 经常使用的、复杂的查询标签化,降低维护成本。
  • 如:GET /trades?status=closed&sort=created,desc
  • 快捷方式:GET /trades#recently-closed
  • 或者:GET /trades/recently-closed

返回有用的错误提示

除了合适的状态码之外,还应该在HTTP响应正文中提供有用的错误提示和详细的描述。这是一个例子。 请求:

  • GET /employees?state=super

响应:

// 400 Bad Request 
{
    "message": "You submitted an invalid state. Valid state values are 'internal' or 'external'",
    "errorCode": 352,
    "additionalInformation":"http://www.domain.com/rest/errorcode/352"
}

对PUT, PATCH和POST请求进行错误验证将需要一个字段分解。下面可能是最好的模式:使用一个固定的顶层错误代码来验证错误,并在额外的字段中提供详细错误信息,就像这样:

{

    "code": 1024,
    "message": "Validation Failed",
    "errors": [{
            "code": 5432,
            "field": "first_name",
            "message": "First name cannot have fancy characters"
        },
        {
            "code": 5622,
            "field": "password",
            "message": "Password cannot be blank"
        }
    ]
}

使用友好的JSON输出

JSON 比XML 可视化更好,也更加节约流量,所以尽量不要使用 XML。

JSON-P

如果在任何GET请求中带有参数callback,且值为非空字符串,那么接口将返回如下格式的数据

$ curl http://api.example.com/#{RESOURCE_URI}?callback=foo

foo({
    "meta": {
        "status": 200,
        "X-Total-Count": 542,
        "Link": [{
                "href": "http://api.example.com/#{RESOURCE_URI}?cursor=0&count=100",
                "rel": "first"
            },
            {
                "href": "http://api.example.com/#{RESOURCE_URI}?cursor=90&count=100",
                "rel": "prev"
            },
            {
                "href": "http://api.example.com/#{RESOURCE_URI}?cursor=120&count=100",
                "rel": "next"
            },
            {
                "href": "http://api.example.com/#{RESOURCE_URI}?cursor=200&count=100",
                "rel": "last"
            }
        ]
    },
    "data": // data
})

使用小驼峰命名法

使用小驼峰命名法作为属性标识符。

{ "yearOfBirth": 1982  }

不要使用下划线(year_of_birth)或大驼峰命名法(YearOfBirth)。通常,RESTful Web服务将被JavaScript编写的客户端使用。客户端会将JSON响应转换为JavaScript对象(通过调用var person = JSON.parse(response)),然后调用其属性。因此,最好遵循JavaScript代码通用规范。

对比:

  • year_of_birth // 不推荐,违反JavaScript代码通用规范
  • YearOfBirth // 不推荐,JavaScript构造方法命名
  • yearOfBirth // 推荐

空字段

接口遵循“输入宽容,输出严格”原则,输出的数据结构中空字段的值一律为 null

缺省情况下确保漂亮的打印和支持gzip

一个提供空白符压缩输出的API,从浏览器中查看结果并不美观。虽然一些有序的查询参数(如?pretty=true)可以提供来使漂亮打印生效,一个默认情况下能进行漂亮打印的API更为平易近人。额外数据传输的成本是微不足道的,尤其是当你比较不执行gzip压缩的成本。

考虑一些用例:假设分析一个API消费者正在调试并且有自己的代码来打印出从API收到的数据——默认情况下这应是可读的。或者,如果消费者抓住他们的代码生成的URL,并直接从浏览器访问它——默认情况下这应是可读的。这些都是小事情。做好小事情会使一个API能被更愉快地使用!

让我们看一个实际例子。我从GitHub API上拉取了一些数据,默认这些数据使用了漂亮打印(pretty print)。我也将做一些GZIP压缩后的对比。

$ curl https://api.github.com/users/veesahni > with-whitespace.txt
$ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt
$ gzip -c with-whitespace.txt > with-whitespace.txt.gz
$ gzip -c without-whitespace.txt ? without-whitespace.txt.gz

输出文件的大小如下:

  • without-whitespace.txt – 1252 bytes
  • with-whitespace.txt – 1369 bytes
  • without-whitespace.txt.gz – 496 bytes
  • with-whitespace.txt.gz – 509 bytes

在这个例子中,当未启用GZIP压缩时空格增加了8.5%的额外输出大小,而当启用GZIP压缩时这个比例是2.6%。另一方面,GZIP压缩节省了60%的带宽。由于漂亮打印的代价相对比较小,最好默认使用漂亮打印,并确保GZIP压缩被支持。

速率限制

为了防止滥用,标准的做法是给API增加某种类型的速率限制。RFC 6585 中介绍了一个HTTP状态码429 请求过多来实现这一点。你可以使用token bucket algorithm技术量化请求限制。

不论怎样,在用户实际受到限制之前告知他们限制的存在是很有用的。这是一个现在还缺乏标准的领域,但是已经有了一些流行的使用HTTP响应头信息的惯用方法。

最少时包含下列头信息(使用Twitter的命名约定来作为头信息,通常没有中间词的大写):

  • X-Rate-Limit-Limit – 当期允许请求的次数
  • X-Rate-Limit-Remaining – 当期剩余的请求次数
  • X-Rate-Limit-Reset – 当期剩余的秒数

为什么对X-Rate-Limit-Reset不使用时间戳而使用秒数?

一个时间戳包含了各种各样的信息,比如日期和时区,但它们却不是必需的。一个API使用者其实只是想知道什么时候能再次发起请求,对他们来说一个秒数用最小的额外处理回答了这个问题。同时规避了时钟偏差的问题。

有些API给X-Rate-Limit-Reset使用UNIX时间戳(纪元以来的秒数)。不要这样做!

为什么对X-Rate-Limit-Reset使用UNIX时间戳是不好的做法?

HTTP 规范已经指定使用RFC 1123 的日期格式 (目前被使用在日期,If-Modified-Since&Last-ModifiedHTTP头信息中)。如果我们打算指定一种使用某种形式时间戳的、新的HTTP头信息,我们应当遵循RFC 1123规定,而不是使用UNIX时间戳。

缓存

HTTP 提供了一套内置的缓存框架! 所有你必须做的是,包含一些额外的出站响应头信息,并且在收到一些入站请求头信息时做一点儿校验工作。

有两种方式:ETagLast-Modified

  • ETag: 当产生一个请求时, 包含一个HTTP 头,ETag会在里面置入一个和表达内容对应的哈希值或校验值。这个值应当跟随表达内容的变化而变化。现在,如果一个入站HTTP请求包含了一个If-None-Match头和一个匹配的ETag值,API应当返回一个304未修改状态码,而不是返回请求的资源。
  • Last-Modified: 基本上像ETag那样工作,不同的是它使用时间戳。在响应头中,Last-Modified包含了一个RFC 1123格式的时间戳,它使用If-Modified-Since来进行验证。注意,HTTP规范已经有了3 种不同的可接受的日期格式 ,服务器应当准备好接收其中的任何一种。

用id来跟踪每次的请求

在每一个API响应中要包含一个Request-Id头信息, 通常用唯一标识UUID. 如果服务器和客户端都打印出他们的Request-Id, 这对我们的网络请求调试和跟踪非常有帮助。

身份验证

一个 RESTful API 应当是无状态的。这意味着认证请求应当不依赖于cookie或session。相反,每一个请求都应当携带某种类型的认证凭证。

由于总是使用SSL,认证凭证能够被简化为一个随机产生的访问令牌,里面传入一个使用HTTP Basic Auth的用户名字段。这样做的极大的好处是,它是完全的浏览器可探测的 – 如果浏览器从服务器收到一个401未授权状态码,它仅需要一个弹出框来索要凭证即可。

然而,这种基于基本认证的令牌的认证方法,仅在满足下列情形时才可用,即用户可以把令牌从一个管理接口复制到API使用者环境。当这种情形不能成立时,应当使用OAuth 2来产生安全令牌并传递给第三方。OAuth 2使用了承载令牌(Bearer tokens) 并且依赖于SSL的底层传输加密。

一个需要支持JSONP的API将需要第三种认证方法,因为JSONP请求不能发送HTTP基本认证凭据(HTTP Basic Auth)或承载令牌(Bearer tokens) 。这种情况下,可以使用一个特殊的查询参数access_token。注意,使用查询参数token存在着一个固有的安全问题,即大多数的web服务器都会把查询参数记录到服务日志中。

这是值得的,所有上面三种方法都只是跨API边界两端的传递令牌的方式。实际的底层令牌本身可能都是相同的。

Token 和 Sign

API 需要设计成无状态,所以客户端在每次请求时都需要提供有效的 Token 和 Sign,在我看来它们的用途分别是:

  • Token 用于证明请求所属的用户,一般都是服务端在登录后随机生成一段字符串(UUID)和登录用户进行绑定,再将其返回给客户端。Token 的状态保持一般有两种方式实现:一种是在用户每次操作都会延长或重置 TOKEN 的生存时间(类似于缓存的机制),另一种是 Token 的生存时间固定不变,但是同时返回一个刷新用的 Token,当 Token 过期时可以将其刷新而不是重新登录。
  • Sign 用于证明该次请求合理,所以一般客户端会把请求参数拼接后并加密作为 Sign 传给服务端,这样即使被抓包了,对方只修改参数而无法生成对应的 Sign 也会被服务端识破。当然也可以将时间戳、请求地址和 Token 也混入 Sign,这样 Sign 也拥有了所属人、时效性和目的地。

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注