RESTful API 设计指南
API 一旦发布其结构将很难修改,因此设计和实现一个符合规范、灵活、友好的 API,是一件非常重要的事情。本节我们来聊聊 API 设计上一些被忽略的细节、使用时的误区和一些实践。
使用名词来表示资源
URI 不应该包含动词。动词应该通过不同的 HTTP 方法来体现,如下是几种常见的错误用法:
GET/getusers/1 POST/users/1/delete POST/users/1/create
正确的用法为:
GET/users/1 DELETE/users/1 PUT/users/1
关注请求头
一定要看请求头信息,并给予正确的状态码。举个例子,假设服务器端只能返回 JSON 格式,如果客户端的头信息的 Accept 字段要求返回 application/xml,这个时候就不应该返回 application/json 类型的数据,而应该返回 406 错误。
合理使用请求方法和状态码
不能一味使用 GET 和 POST,返回 200,不合理的请求方法可能让未来的维护者或者合作方感到迷惑。关于方法语义的说明,可参考表 5.1。
表 5.1 方法语义的说明
方法 | 语义 |
OPTIONS | 用于获取资源支持的所有 HTTP 方法 |
HEAD | 用于只获取请求某个资源返回的头信息 |
GET | 用于从服务器获取某个资源的信息: 1.完成请求后,返回状态码 200 OK 2.完成请求后,需要返回被请求的资源详细信息 |
POST | 用于创建新资源: 1.创建完成后,返回状态码 201 Created 2.完成请求后,需要返回被创建的资源详细信息 |
PUT | 用于完整的替换资源或者创建指定身份的资源,比如创建 id 为 123 的某个资源: 1.如果是创建了资源,则返回 201 Created 2.如果是替换了资源,则返回 200 OK |
PATCH | 用于局部更新资源: 1.完成请求后,返回状态码 200 OK 2.完成请求后,需要返回被修改的资源详细信息 3.完成请求后,需要返回被修改的资源详细信息 |
DELETE | 用于删除某个资源,完成请求后返回状态码 204 No Content |
正确地使用 REST
REST 服务器是无状态的。在有分页的时候,它并不能知道你当前访问到了什么位置,前一页和后一页的地址是什么,这个关系需要客户端来维护。但是“下一页资源”这样的业务逻辑是需要服务端来提供的。例如,下面例子的返回是不完整的:
Status:200 OK [ { "id":1, "url":"https://api.dongwm.com/users/1/", }, { "id":2, "url":"https://api.dongwm.com/users/2/", } ]
返回的结果没有告诉我们是否有下一页,也没有告诉我们符合条件的记录总数。可以添加 Link 和 X-Total-Count 头来提供这样的功能:
Status:200 OK X-Total-Count:210 Link:<https://api.dongwm.com/users?page=2>;rel="next", <https://api.dongwm.com/users?page=5>;rel="last" [ { "id":1, "url":"https://api.dongwm.com/users/1/", }, { "id":2, "url":"https://api.dongwm.com/users/2/", } ]
rel 的值还可以是 first、self 和 prev,客户端只需要根据 Link 中提供的链接就可以找到全部的符合条件的条目。
还有两处容易出错的地方需要留意:
- 使用“201 Created”响应时,应该带 Location,指向新建资源的地址。
- 使用“405 Method Not Allowed”响应时,应该带有 Allow 头,告诉客户端对该资源有效的 HTTP 方法。
对输出的结果不再包装
body 中应该直接放数据,不要多层封装。下面有个不恰当的响应的例子:
HTTP/1.1 200 OK { 'success':true, 'data':{'id':1, 'name':'xiaoming'}, }
直接返回 data 中的数据就好了:
HTTP/1.1 200 OK {'id':1, 'name':'xiaoming'}
因为通过状态码“200 OK”就可以知道结果是正确的,也没有必要添加“success”字段。
如果 API 使用者确实由于某种原因无法访问返回头,或者 API 需要支持交叉域请求(例如通过 jsonp),这两种情况下还是需要包装的。
不要做出错误的提示
当访问出错或者响应的结果不符合预期时,不应该返回 200 作为状态码。哪怕返回的结果中也包含了错误原因,因为在没有充分的文档说明前提下,客户端可能会缓存成功的 HTTP 请求。
使用嵌套对象序列化
对象应该合理地嵌套,不应该都在一个层次上。如下的格式是不正确的:
{ 'id':1, 'post_id':'10001', 'post_name':'Post1', 'post_content':'this is a post' }
尽可能把相关联的资源信息内联在一起。应该把 post 作为一个键:
{ 'id':1, 'post':{ 'id':'10001', 'name':'Post1', 'content':'this is a post' } }
版本
常见的区分版本的方法有三种:
- 保存在 URI 中。比如“https://api.dongwm.com/api/v2”。
- 放在请求头中。比如 GitHub 的用法:“Accept:application/vnd.github.v3+json”。
- 自定义请求头。比如,“X-Api-Version:1”。
第三种方式不推荐,推荐使用第一种。
URI 失效和迁移
随着业务发展,会出现一些 API 失效或者迁移。对失效的 API,应该返回“404 not found”或“410 gone”;对迁移的 API,返回 301 重定向。
信息过滤
URL 通常最好越简短越好,对结果过滤、排序和搜索相关的功能都应该通过参数实现。一些常见的参数用法如表 5.2 所示。
表 5.2 常见的参数及其用法
参数 | 含义 |
offset=0&limit=10 | 指定返回记录的数量,offset 也可以用 start 这个名字 |
offset=10 | 指定返回记录的开始位置 |
page=2&per_page=100 | 指定第几页,以及每页的记录数 |
sortby=name&order=asc | 指定返回结果按照哪个属性排序,以及排序的顺序 |
sort=age,desc | 多个排序条件组合 |
速度限制
为了避免请求泛滥,给 API 设置速度限制很重要。为此,RFC 6585(https://tools.ietf.org/html/rfc6585 )引入了 HTTP 状态码 429(too many requests)。加入速度限制功能之后,应该提示用户。可以参照 GitHub 的返回头,如下所述。
- X-RateLimit-Limit:当前时间段允许的并发请求数。
- X-RateLimit-Remaining:当前时间段保留的请求数。
- X-RateLimit-Reset:当前时间段剩余的秒数。
我们看一个真实的例子:
> curl-i https://api.github.com/users/whatever HTTP/1.1 200 OK Date:Mon, 01 Jul 2013 17:27:06 GMT Status:200 OK X-RateLimit-Limit:60 X-RateLimit-Remaining:56 X-RateLimit-Reset:1372700873
缓存
数据内容在一段时间不会变动,这个时候我们就可以合理地减少 HTTP 响应内容。应该在响应头中携带 Last-Modified、ETag、Vary、Date 等信息,客户端可以在随后请求这些资源时,在请求头中使用 If-Modified-Since、If-None-Match 等来确认资源是否经过修改。如果资源没有做过修改,那么就可以响应“304 Not Modified”,并且不在响应实体中返回任何内容。
我们看看 GitHub 的用法(隐藏了无关的自定义头):
> http https://api.github.com/users/dongweiming--headers HTTP/1.1 200 OK Cache-Control:public, max-age=60, s-maxage=60 Content-Encoding:gzip Content-Security-Policy:default-src 'none' Content-Type:application/json;charset=utf-8 Date:Thu, 04 Feb 2016 14:05:03 GMT ETag:W/"393f2b88fc9073927d2dda6f43318c8a" Last-Modified:Sat, 16 Jan 2016 09:14:15 GMT Server:GitHub.com Status:200 OK Transfer-Encoding:chunked Vary:Accept Vary:Accept-Encoding
我们可以通过 If-Modified-Since 实现缓存:
> http https://api.github.com/users/dongweiming 'If-Modified-Since:Thu, 04 Feb 2016 14:05:03 GMT'--headers HTTP/1.1 304 Not Modified Cache-Control:public, max-age=60, s-maxage=60 Content-Security-Policy:default-src 'none' Date:Thu, 04 Feb 2016 14:06:43 GMT Last-Modified:Sat, 16 Jan 2016 09:14:15 GMT Server:GitHub.com Status:304 Not Modified Vary:Accept-Encoding
当生成请求的时候,在 HTTP 头里面加入 ETag。其中包含请求的校验和与哈希值,这个值在输入变化的时候也应该变化。如果输入的 HTTP 请求包含 If-None-Match 头以及一个 ETag 值,那么 API 应该返回“304 Not Modified”状态码,而不是常规的输出结果:
> http https://api.github.com/users/dongweiming 'If-None-Match:"393 f2b88fc9073927d2dda6f43318c8a"'--headers HTTP/1.1 304 Not Modified Cache-Control:public, max-age=60, s-maxage=60 Content-Security-Policy:default-src 'none' Date:Thu, 04 Feb 2016 14:07:17 GMT ETag:"393f2b88fc9073927d2dda6f43318c8a" Last-Modified:Sat, 16 Jan 2016 09:14:15 GMT Server:GitHub.com Status:304 Not Modified Vary:Accept-Encoding
并发控制
缺少并发控制的 PUT 和 PATCH 请求可能导致“更新丢失”。这个时候可以使用 Last-Modified 和 ETag 头来实现条件请求。具体原则如下:
- 客户端发起的请求如果没有包含 If-Unmodified-Since 或者 If-Match 头,就返回状态码“403 Forbidden”,在响应正文中解释为何返回该状态码。
- 客户端发起的请求所提供的 If-Unmodified-Since 或者 If-Match 头与服务器记录的实际修改时间或 ETag 值不匹配时,返回状态码“412 Precondition Failed”。
- 客户端发起的请求所提供的 If-Unmodified-Since 或者 If-Match 头与服务器记录的实际修改时间或 ETag 的历史值匹配,但资源已经被修改过时,返回状态码“409 Conflict”。
- 客户端发起的请求所提供的条件符合实际值,就更新资源,响应“200 OK”或者“204 No Content”,并且包含更新过的 Last-Modified 和/或 ETag 头,同时包含 Content-Location 头,其值为更新后的资源 URI。
本节参考了开源项目“HTTP 接口设计指北”(http://bit.ly/2azgIBP ),如果想了解更多内容,请阅读此项目。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论