w3ctech

RESTful Web API的设计思路

目的, 适用人群以及其它内容

针对(如何)设计RESTful Web APIs问题,我决定写一篇文章(可以用作如何快速入门RESTful Web APIS的教程),就拿文章内容来说,它的关注点在于如何设计RESTful Web APIs,而不是探寻为什么要这样设计RESTful Web APIs,针对后者(要探寻为什么这样设计RESTful Web APIs)的读者,建议你们先核实一下文章的目录(言外之意:文章根本就没有关于为什么这样设计RESTful Web APIs的章节)。

一切皆资源

和RESTful API之间的任何交互都可以看作是在和资源进行交互。实际上,RESTful API可以简单理解:对资源建立映射然后获取资源的结束位置,或者对资源进行唯一标识(URL)。资源通常是我们获取文档内容或者服务等信息的源头。用户也可以被看作一种资源,于是用户就拥有唯一的URL标识,例如Github的例子:

https://api.github.com/users/lrei

资源可以拥有不同的表现形式。上面提到的用户可以拥有下面列出的JSON表现形式(部分内容):

{
    "login": "lrei",
    "created_at": "2008-11-21T14:48:42Z",
    "name": "Luis Rei",
    "email": "me@luisrei.com",
    "id": 35857,
    "blog": "http://luisrei.com"
}

正确的HTTP行为: 使用HTTP内置的动词以及利用响应中的状态码以及状态码描述

资源是名词!下面所虚构出的API URL:

http://api.example.com/posts/delete/233/

其实是错误的。delete明显是一种动作,而不是一种资源。而是应该使用HTTP内置动词或者HTTP内置请求方法的方式来展示RESTful web服务中的行为:

HTTP动词    行为 (常规用法)
GET         获取资源的展现形式,该资源没有副作用(不会改动服务器上任何东西)
HEAD        获取资源中的meta信息(也即headers),举个例子,HEAD和GET很类似,但是HEAD没有响应体以及没有副作用
OPTIONS     返回一个动作(用来支持特定且无副作用的资源)
POST        创建资源
PUT         完全替换已存在的资源
PATCH       对资源部分修改
DELETE      删除资源.

当我们在使用PUT,POST,或者PATCH等HTTP行为来发送数据(被放置在请求的body体)。不要使用在URL中添加查询参数的方式来改变HTTP状态。下面列举的是来自GitHub API的例子:

POST https://api.github.com/gists/gists
{
    "description": "the description for this gist",
    "public": true,
    "files": {
    "file1.txt": {
        "content": "String file contents"
        }
    }

}

使用HTTP响应报头自带的状态码以及产生该状态码的原因描述的方式也可以很好的对请求作出响应。(使用这种方式的话),那么针对先前的请求所做出的响应包含以下的header头:

201 Created
Location: https://api.github.com/gists/1

假设随后进行下列的HTTP请求:

GET https://api.github.com/gists/1

我们期望响应中的状态码是:

200 OK

RESTful HTTP服务端应用会根据HTTP规范必须返回状态码。针对POST请求的响应体是否要返回已创建的资源的选项是可选的,然而响应体一定要返回已创建资源的位置。

除去资源以及行为,剩下的都是属于headers头部

资源映射到URLs,行为映射到HTTP动词,剩下的部分包含在headers头部。

Content Negotiation

使用HTTP content negotiation,会产生选择同一资源会对应不同的展现形式等问题。

GET https://api.github.com/gists/1
Accept: application/json

上面获取的是资源的JSON展现形式

GET https://api.github.com/gists/1
Accept: application/xml

上面获取的是资源的XML展现形式。接下来的是对请求响应后的结果:

200 OK
Content-Type: application/json; charset=utf-8
(response body)

这是因为存在可以访问的JSON资源

406 Not Acceptable
Content-Type: application/json
{
"message": "Must ACCEPT application/json: [\"application/xml\"]"
}

这是因为GitHub gists目前不存在可以访问的XML资源。请注意,服务器可以选择任意表现形式的406响应体,在这种情况下,尽管客户端请求的内容类型是其它类型(例如XML),但是响应报头Content-Type还是JSON。 从更广义层面来讲,表现形式不仅仅是关于文件的格式,表现形式也被常用做压缩或者选择不同的语言:

GET /resource
Accept-Language: en-US
Accept-Charset: iso-8859-5
Accept-Encoding: gzip  

更重要的,HTTP请求中的Accept头可以处理RESTful Web APIs:媒体类型版本控制器。这里有个既应用资源展现形式又使用媒体类型的版本号的例子(来自spire.io):

 Accept: application/vnd.spire-io.session+json;version=1.0

当服务器不再支持特别的版本号,服务器给出的响应大概是

415 Media Type Not Supported

缓存化

主要有两种HTTP响应头来控制缓存行为:过期时间可以用来确定已缓存实体的失效时间以及缓存控制通过使用max-age指令就可以确定相对应的失效时间。 过期时间和缓存控制都结合使用ETag(实体tag,一种标识,常见形式为hash值,随着资源的变化而变化),会导致缓存实体的版本号失效或者Last-Modified(头部记录的是资源上次修改的时间)失效。 请求:

GET https://api.github.com/gists/1
Accept: application/json

响应:

200 OK
ETag: "2259b5bea67655550030acf98bad4184"

接下来的HTTP请求使用(下面的HTTP请求头)

GET https://api.github.com/gists/1
Accept: application/json
If-None-Match: "2259b5bea67655550030acf98bad4184"

返回

304 Not Modified

没有响应体。可以用Last-Modified/If-Modified-Since头来实现相同的功能。

权限

尝试在GitHub创建一个repo代码库:

POST https://api.github.com/user/repos

以及响应:

401 Unauthorized
WWW-Authenticate: Basic realm="GitHub"

这就意味着,只有获得基本的HTTP认证(其实就是对"用户名:密码"字符串进行base64编码的HTTP认证头部),才能对/user/repos发起有效的POST请求:

POST https://api.github.com/user/repos
Authorization: Basic bHJlaTp5ZWFocmlnaHQ=

基本的HTTP认证需要经常会结合SSL/TLS(HTTPS)使用,否则用户名/密码很容易被劫持。 针对使用OAuth2文档已有api的第三方应用来说,OAuth2一种比较常见的方式。

限速

HTTP目前没有针对限速的任何规范。GitHub使用:

GET https://api.github.com/gists/1

或者其它任何请求(能够返回下面的响应头):

200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4966

上面的响应头表明,客户端发送请求的速度被限定在5000请求/小时之内,在接下来的一个小时,客户端能够发起4966请求。按照惯例,非标准的HTTP头部通常会有X-前缀。 众所周知,针对限速这块做得最好的可能是twitter。twitter实现的限速模块和GitHub类似,并且包含X-RateLimit-Reset(表明限速可以被重置)。额外的限制也会影响特殊的API调用或者一系列的API调用。Twitter实现了具有X-FeatureRateLimit-Limit, X-FeatureRateLimit-Remaining and X-FeatureRateLimit-Reset的HTTP模块。 当我们因为限速的原因拒绝某个请求,响应头的状态码应该是403 Forbidden,响应头会使用一段信息说明拒绝的原因。

超文本驱动,可选项以及错误处理

REST API一定不要把资源的名字或者层级(因为有可能存在几个客户端和服务器端)固定死。服务器必须能自由地控制它们的命名空间(...),这里的失败就是用来提示,客户端正利用不同频道信号传输信息(...)来假定资源结构。REST API理应被设计成不需要预先知识就能上手,这一点就远超最初的URI设计理念。-Roy Thomas Fielding(我要强调的地方) 换句话说,基于URL(除原始的URL以外)所构建出真实的REST API URL都可以被修改,即使是对其它服务器来说,不用担心客户端。 Hypermedia(应用状态引擎,也即超文本驱动)可以减少某想法(是否将API当作状态机)的思考次数,(说白了),在状态机里面,你可以把资源当作一种状态,状态之间的过渡是资源之间建立联系的纽带,并且状态都被保存在各自的资源实体中(hypermedia) 根URL是第一种状态。所以让我们用Spire.io的API的根URL开始吧:

GET https://api.spire.io

结果:

200 OK
{
    "url": "https://api.spire.io/",
    "resources": {
    "sessions": {
        "url": "https://api.spire.io/sessions"
     },
    "accounts": {
        "url": "https://api.spire.io/accounts"
     },
    "billing": {
        "url": "https://api.spire.io/billing"
     }
}
…

根资源或者原始状态中,都存在可以通过"sessions","account","billing"等方式来获取其它资源(或者状态)的过渡时期。 当然HTTP响应Link头部也有可能存在过渡时期,举个例子:

GET /gists/starred

导致接下来的响应:

200 OK
Link: ; rel="next",
  ; rel="last"

在没有hypermedia资源(例如图片)的情况下,http头部才是实现资源(如状态之间的过渡)添加API 元数据的唯一途径。即使是某想法(不需要解析请求体也能实现状态之间过渡)变成可能,我也认为在请求头部添加transitions是一种不错的想法。

选项

在给定资源的前提下,如何对资源进行正确的操作才是最有用的价值信息。(针对上面的问题),我们发现使用HTTP请求中OPTIONS方法是一种解决途径:

OPTIONS https://api.spire.io/accounts

响应:

Status Code: 200
access-control-allow-methods: GET,POST
access-control-allow-origin: *

HTTP/1.1方法定义可以看出: 针对使用HTTP OPTIONS方法请求所返回的200响应来说,应该包含所有的HTTP头部字段(可以知道服务器实现哪些可选的功能)以及适用于所有的资源(如Allow),甚至需要包含规范中没有定义的扩展。 在没有必要考虑发起请求是否不符合某些限制要求的情况下,OPTIONS似乎是一个可以包含关于资源针对特定速度约束的好地方。

错误处理

下面列举的都是错误的响应:

  • 含有HTTP关于错误的状态码以及相关的响应头;
  • 以不正确的格式读取人们可读的信息(包括链接读取文档内容);
  • 在某些场景使用Link头部(包含有意义的状态过渡);

使用来自twilio api的例子:

GET https://api.twilio.com/2010-04-01/Accounts.json

结果:

401 Unauthorized
WWW-Authenticate: Basic realm="Twilio API"
{
    "status": 401,
    "message": "Authenticate",
    "code": 20003,
    "more_info": "http:\/\/www.twilio.com\/docs\/errors\/20003"
}
w3ctech微信

扫码关注w3ctech微信公众号

共收到1条回复

  • 亮亮,good

    回复此楼