我所认为的RESTful API最佳实践

标签: restful api 最佳实践 | 发表时间:2016-04-03 01:45 | 作者:
出处:http://www.scienjus.com/

不要纠结于无意义的规范

在开始本文之前,我想先说这么一句:RESTful 真的很好,但它只是一种软件架构风格,过度纠结如何遵守规范只是徒增烦恼,也违背了使用它的初衷。

就像 Elasticsearch 的 API 会在 GET 请求中直接传 JSON,但这是它的业务需要,因为普通的 Query Param 根本无法构造如此复杂的查询 DSL。Github 的 V3 API 中也有很多不符合标准的地方,这也并不会妨碍它成为业界 RESTful API 的参考标准。

我接下来要介绍的一些东西也会跟标准不符,但这是我在实际开发中遇到过、困扰过、思考过所得出的结论,所以才是我所认为的RESTful API 最佳实践。

为什么要用 RESTful

RESTful 给我的最大感觉就是规范、易懂和优雅,一个结构清晰、易于理解的 API 完全可以省去许多无意义的沟通和文档。并且 RESTful 现在越来越流行,也有越来越多优秀的周边工具(例如文档工具 Swagger)。

协议

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

版本

API 的版本号和客户端 APP 的版本号是毫无关系的,不要让 APP 将它们用于提交应用市场的版本号传递到服务器,而是提供类似于 v1v2之类的 API 版本号。版本号只允许枚举,不允许判断区间。

版本号拼接在 URL 中或是放在 Header 中都可以。例如:

     
1
     
api.xxx.com/v1/users

或:

     
1
2
3
     
api.xxx.com/users
version=v1

请求

一般来说 API 的外在形式无非就是增删改查(当然具体的业务逻辑肯定要复杂得多),而查询又分为详情和列表两种,在 RESTful 中这就相当于通用的模板。例如针对文章(Article)设计 API,那么最基础的 URL 就是这几种:

  • GET /articles: 文章列表
  • GET /articles/id:文章详情
  • POST /articles/: 创建文章
  • PUT /articles/id:修改文章
  • DELETE /articles/id:删除文章

RESTful 中使用 GET、POST、PUT 和 DELETE 来表示资源的查询、创建、更改、删除,并且除了 POST 其他三种请求都具备幂等性(多次请求的效果相同)。需要注意的是 POST 和 PUT 最大的区别就是幂等性,所以 PUT 也可以用于创建操作,只要在创建前就可以确定资源的 id。

将 id 放在 URL 中而不是 Query Param 的其中一个好处是可以表示资源之间的层级关系,例如文章下面会有评论(Comment)和点赞(Like),这两项资源必然会属于一篇文章,所以它们的 URL 应该是这样的:

评论:

  • GET /articles/aid/comments: 某篇文章的评论列表
  • GET /comments/cid: 获取
  • POST /articles/aid/comments: 在某篇文章中创建评论
  • PUT /comments/cid: 修改评论
  • DELETE /comments/cid: 删除评论

    这里有一点比较特殊,永远去使用可以指向资源的的最短 URL 路径,也就是说既然 /comments/cid已经可以指向一条评论了,就不需要再用 /articles/aid/comments/cid特意的指出所属文章了。

    点赞:

  • GET /articles/id/like:查看文章是否被点赞

  • PUT /articles/id/like:点赞文章
  • DELETE /articles/id/like:取消点赞

RESTful 中不建议出现动词,所以可以将这种关系作为资源来映射。并且由于大部分的关系查询都与当前的登录用户有关,所以也可以直接在关系所属的资源中返回关系状态。例如点赞状态就可以直接在获取文章详情时返回。注意这里我选择了 PUT 而不是 POST,因为我觉得点赞这种行为应该是幂等的,多次操作的结果应该相同。

Token 和 Sign

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

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

统计性参数

我不太清楚这类参数具体该被称为什么,总之就是用户的各种隐私【误。类似于经纬度、手机系统、型号、IMEI、网络状态、客户端版本、渠道等,这些参数会经常收集然后用作运营、统计等平台,但是在大部分情况下他们是与业务无关的。这类参数变化不频繁的可以在登录时提交,变化比较频繁的可以用轮训或是在其他请求中附加提交。

业务参数

在 RESTful 的标准中,PUT 和 PATCH 都可以用于修改操作,它们的区别是 PUT 需要提交整个对象,而 PATCH 只需要提交修改的信息。但是在我看来实际应用中不需要这么麻烦,所以我一律使用 PUT,并且只提交修改的信息。

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

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

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

     
1
     
/users/?query=ScienJus

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

     
1
     
/users/?gender=1

对于某些特定且复杂的业务逻辑,不要试图让客户端用复杂的查询参数表示,而是在 URL 使用别名:

     
1
     
/users/recommend

分页:

     
1
2
3
4
5
     
/users/?offset=10&limit=10
/articles/?cursor=2015-01-01 15:20:30&limit=10
/users/?page=2&pre_page=20

排序,只需要对已有的情况进行支持:

     
1
     
/articles/sort=-create_date

PS:我很喜欢这种在字段名前面加 -表示降序排列的方式。

响应

尽量使用 HTTP 状态码,常用的有:

  • 200:请求成功
  • 201:创建、修改成功
  • 204:删除成功
  • 400:参数错误
  • 401:未登录
  • 403:禁止访问
  • 404:未找到
  • 500:系统错误

    但是有些时候仅仅使用 HTTP 状态码没有办法明确的表达错误信息,所以我倾向于在里面再包一层自定义的返回码,例如:

    成功时:

     
1
2
3
4
5
     
{
"code": 100,
"msg": "成功",
"data": {}
}

失败时:

     
1
2
3
4
     
{
"code": -1000,
"msg": "用户名或密码错误"
}

data是真正需要返回的数据,并且只会在请求成功时才存在, msg只用在开发环境,并且只为了开发人员识别。客户端逻辑只允许识别 code,并且不允许直接将 msg的内容展示给用户。如果这个错误很复杂,无法使用一段话描述清楚,也可以在添加一个 doc字段,包含指向该错误的文档的链接。

返回数据

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

创建和修改操作成功后,需要返回该资源的全部信息。

返回数据不要和客户端界面强耦合,不要在设计 API 时就考虑少查询一张关联表或是少查询 / 返回几个字段能带来多大的性能提升。并且一定要以资源为单位,即使客户端一个页面需要展示多个资源,也不要在一个接口中全部返回,而是让客户端分别请求多个接口。

最好将返回数据进行加密和压缩,尤其是压缩在移动应用中还是比较重要的。

分页

APP 后端分页设计 中提到过,分页布局一般分为两种,一种是在 Web 端比较常见的有底部分页栏的电梯式分页,另一种是在 APP 中比较常见的上拉加载更多的流式分页。这两种分页的 API 到底该如何设计呢?

电梯式分页需要提供 page(页数)和 pre_page(每页的数量)。例如:

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

而服务端则需要额外返回 total_count(总记录数),以及可选的当前页数、每页的数量(这两个与客户端提交的相同)、总页数、是否有下一页、是否有上一页(这三个都可以通过总记录数计算出)。例如:

     
1
2
3
4
5
6
7
8
9
10
11
     
{
"pagination": {
"previous": 1,
"next": 3,
"current": 2,
"per_page": 20,
"total": 200,
"pages": 10
},
"data": {}
}

流式布局也完全可以使用这种方式,并且不需要查询总记录数(好处是减少一次数据库操作,坏处时客户端需要多请求一次才能判断是否到最后一页)。但是会出现数据重复和缺失的情况,所以更推荐使用游标分页。

游标分页需要提供 cursor(下一页的起点游标) 和 limit(数量) 参数。例如:

     
1
     
/articles/?cursor=2015-01-01 15:20:30&limit=10

如果文章列表默认是以创建时间为倒序排列的,那么 cursor就是当前列表最后一条的创建时间(第一页为当前时间)。

服务端需要返回的数据也很简单,只需要以此游标为起点的总记录数和下一个起点游标就可以了。例如:

     
1
2
3
4
5
6
7
8
     
{
"pagination": {
"next": "2015-01-01 12:20:30",
"limit": 10,
"total": 100,
},
"data": {}
}

如果 total小于 limit,就说明已经没有数据了。

流式布局的分页 API 还有一种情况很常见,就是下拉刷新的增量更新。它的业务逻辑正好和游标分页相反,但是参数基本一样:

     
1
     
/articles/?cursor=2015-01-01 15:20:30&limit=20

返回数据有两种可能,一种是增量更新的数据小于指定的数量,就直接将全部数据返回(这个数量可以设置的相对大一些),客户端会将这些增量更新的数据添加在已有列表的顶部。但是如果增量更新的数据要大于指定的数量,就会只返回最新的 n 条数据作为第一页,这时候客户端需要清空之前的列表。例如:

     
1
2
3
4
5
6
7
     
{
"pagination": {
"limit": 20,
"total": 100,
},
"data": {}
}

如果 total大于 limit,说明增量的数据太多所以只返回了第一页,需要清空旧的列表。

相关 [restful api 最佳实践] 推荐:

RESTful API 设计最佳实践

- - 文章 – 伯乐在线
项目资源的URL应该如何设计. 用哪种HTTP方法来创建一个新的资源. 实现分页和版本控制的最好方法是什么. 因为有太多的疑问,设计RESTful API变得很棘手. 在这篇文章中,我们来看一下RESTful API设计,并给出一个最佳实践方案. 资源集合用一个URL,具体某个资源用一个URL:. #资源集合的URL /employees/56.

我所认为的RESTful API最佳实践

- - ScienJus's Blog
在开始本文之前,我想先说这么一句:RESTful 真的很好,但它只是一种软件架构风格,过度纠结如何遵守规范只是徒增烦恼,也违背了使用它的初衷. 就像 Elasticsearch 的 API 会在 GET 请求中直接传 JSON,但这是它的业务需要,因为普通的 Query Param 根本无法构造如此复杂的查询 DSL.

RESTful API 设计指南

- - 阮一峰的网络日志
网络应用程序,分为前端和后端两个部分. 当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......). 因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信. 这导致API构架的流行,甚至出现 "API First"的设计思想. RESTful API是目前比较成熟的一套互联网应用程序的API设计理论.

RESTful API版本控制策略

- - ITeye博客
做RESTful开放平台,一方面其API变动越少, 对API调用者越有利;另一方面,没有人可以预测未来,系统在发展的过程中,不可避免的需要添加新的资源,或者修改现有资源. 因此,改动升级必不可少,但是,作为平台开发者,你必须有觉悟:一旦你的API开放出去,有人开始用了,你就不能只管自己Happy了,你对平台的任何改动都需要考虑对当前用户的影响.

【翻译】优秀的RESTful API的设计原则

- - 行业应用 - ITeye博客
原文地址: http://codeplanet.io/principles-good-restful-api-design/. 原文作者:Thomas Hunter Ii. 第一次翻译,若有不对的地方,敬请谅解,若转载请注明本文地址. 定义(Definitions). 数据的设计与抽象化(Data Design and Abstraction).

Apache Hadoop 1.0.0支持Kerberos验证,支持Apache HBase,提供针对HDFS的RESTful API

- - InfoQ中文站
海量数据框架Apache Hadoop怀胎六年终于瓜熟蒂落发布1.0.0版本. 本次发布的核心特性包括支持Kerberos身份验证,支持Apache HBase,以及针对HDFS的RESTful API. InfoQ就此次发布请Apache Hadoop项目的VP——Arun Murthy回答了几个问题.

jQuery最佳实践

- andi - 阮一峰的网络日志
上周,我整理了《jQuery设计思想》. 那篇文章是一篇入门教程,从设计思想的角度,讲解"怎么使用jQuery". 今天的文章则是更进一步,讲解"如何用好jQuery". 我主要参考了Addy Osmani的PPT《提高jQuery性能的诀窍》(jQuery Proven Performance Tips And Tricks).

PHP最佳实践

- xiangqian - 阮一峰的网络日志
虽然名字叫《PHP最佳实践》,但是它主要谈的不是编程规则,而是PHP应用程序的合理架构. 它提供了一种逻辑和数据分离的架构模式,属于MVC模式的一种实践. 我觉得,这是很有参考价值的学习资料,类似的文章网上并不多,所以一边学习,一边就把它翻译了出来. 根据自己的理解,我总结了它的MVC模式的实现方式(详细解释见译文):.

MongoDB最佳实践

- - NoSQLFan
将 MongoDB加入到我们的服务支持列表中,是整个团队年初工作计划中的首要任务. 但我们感觉如果先添加一项对NoSQL存储的支持,而不是先升级已支持的关系型数据库,可能对用户不太好,毕竟目前的用户都使用关系型数据库. 所以我们决定将引入MongoDB这项工作放到升级MySQL和PostgreSQL之后来做.

理解RESTful架构

- InterMa - 阮一峰的网络日志
越来越多的人开始意识到,网站即软件,而且是一种新型的软件. 这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency)、高并发等特点. 网站开发,完全可以采用软件开发的模式. 但是传统上,软件和网络是两个不同的领域,很少有交集;软件开发主要针对单机环境,网络则主要研究系统之间的通信.