RESTful最佳实践

什么是RESTFUL

RESTFUL是一种软件架构风格,也可以理解为一种协议约束。

设计规范

协议

http/https

版本

版本通常放到url或header中

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

URL

rest风格中URL表示资源,使用名词而不使用动词
所有资源应该一致使用复数

RESTful API应具备良好的可读性,当url中某一个片段由多个单词构成,建议使用-来隔断单词,而不是_

1
2
3
4
5
# good
/api/featured-post/
# bad
/api/featured_post/

请求方式

1
2
3
4
5
6
GET /articles
GET /articles/:id
POST /articles/
PUT /articles/:id 提供完整数据内容
PATCH /articles/:id 只提供改动内容部分
DELETE /articles/:id

组合资源要避免嵌套

例1:

1
2
3
4
5
GET /articles/:aid/comments 某篇文章的评论列表
POST /articles/:aid/comments 在某篇文章中创建评论
GET /comments/:cid 获取某个评论详情
PUT /comments/:cid 修改评论
DELETE /comments/:cid 删除评论

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

例2:
一个系统里面包含多个 applications,一个 application 又包含多个 users。那获取 user 资源的路径应该是怎样的?

先看一个错误用法:

1
GET http://~/$version/systems/:systemId/applications/:applicationId/users/:userId

上述构造方式会让接口变得混乱和缺少灵活性。正确做法是:

1
2
3
GET http://~/$version/systems/:systemId
GET http://~/$version/applications/:applicationId
GET http://~/$version/users/:userId/

点赞

1
2
3
GET /articles/:id/like 查看文章点赞信息
PUT /articles/:id/like 点赞文章
DELETE /articles/:id/like 取消点赞

Token和Sign

API需要设计成无状态的,所以客户端每次请求都要提供有效的Token和Sign

Token用于证明请求所属的用户。
Sign用于证明此次请求合理,一般客户端会把请求参数拼接解密后作为Sign传给服务器。

业务参数

在RESTFUL标准中,PUT和PATCH都可以用于修改操作,它们的区别是PUT需要提交整个对象,而PATCH只需要提交修改的信息。通常只需要实现PUT就好。

另一个问题是POST创建对象时,使用表单提交还是json提交更好些。其实两者都行,唯一区别是json可以很方便的表示复杂的结构。

另外一个建议是最好将过滤、分页和排序等全权交给客户端,包括过滤条件、页数、每页数量、排序方式、升降序等,这样可以使API更加灵活。但是对于过滤条件、排序方式等,不需要支持所有方式,只需要支持目前用得上的和以后可能用得上的方式即可。
例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
搜索
/users/?query=ScienJus
过滤
/users/?gender=1
对于特定且复杂的业务逻辑,不要试图让客户端用复杂的查询参数表示,而是在URL使用别名
/users/recommend
分页
/users/?offset=10&limit=10
/users/?page=2&page_size=20
排序
/articles/?sort=-create_date

响应

尽量使用HTTP状态码,常用有
200:请求成功
201:创建、修改成功
204:删除成功
400:参数错误
401:未登录
403:禁止访问
404:未找到
500:服务器内部错误

为了表达更准确的错误信息,倾向于返回信息中再包一层返回码
成功时

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到底该如何设计呢?

电梯式分页

电梯式分页需要提供pagepage_size

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

而服务端需要额外返回total_count,以及当前所在页、每页的数量、总页数,可选的还可提供是否有上一页、是否有下一页、总页数信息

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

流式布局分页

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

上滑
游标分页需要提供cursor(下一页的起点游标)和limit参数。

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

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

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,说明增量的数据太多所以只返回了第一页,需要清空旧的列表。

频率限制

处于防止恶意访问和服务器性能压力考虑,限制API访问频次是非常有必要的,如果一个客户端请求API频率太快,根据HTTP协议,可以返回429(too many requests)

如果要为客户端提供更加详细的调用频次和访问次数之类的信息,除了提供文档说明外,还可以在http header用自定义字段的形式提供,比如Twitter api是这么做的

X-Rate-Limit-Limit:该请求的调用上限
X-Rate-Limit-Remaining:15分钟内还可调用的次数
X-Rate-Limit-Reset:还有多少秒后访问限制会被重置

参考

我所认为的RESTful API最佳实践
RESTful API的十个最佳实践
REST与RESTFul API最佳实践
RESTful Best Practices
豆瓣开放平台V2接口
githubV3接口

RESTful Service API 设计最佳工程实践和常见问题解决方案