REST全称是Representational State Transfer,中文意思是表述(通常译为表征)状态转移。
如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构,下面介绍RESTful API接口设计原则
接口设计具体还是要根据需求来定义,需要保证原子性的情况比较多,比如设计几个操作一部分成功一部分失败应该统一定义成失败,把成功的部分回滚还可以者先处理失败概率比较大的操作再处理失败概率小的操作。
URL设计
URL地址
小写字母多个单词使用下划线分隔。?后面跟的参数使用驼峰模式
动词+宾语
Restful的核心思,比如 GET /article
,GET
是动词,article
是宾语。
1 | GET (read) |
动词覆盖
有些客户端只能使用GET和POST这两种方法。服务器必须接受POST模拟其他三个方法(PUT、PATCH、DELETE)。
这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override属性,告诉服务器应该使用哪一个动词,覆盖POST方法。
1 | POST /api/article/4 HTTP/1.1 |
上面代码中,X-HTTP-Method-Override指定本次请求的方法是PUT,而不是POST。
宾语必须是名词
宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/article这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的:
1 | /get_all_article |
不使用复数URL
避免多级URL
常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。(当然使用多层级的系统也很多)
1 | GET /author/12/categorie/2 |
这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。
更好的做法是,除了第一级(也可以从第二级开始),其他级别都用查询字符串表达。
1 | GET /author/12?categorie=2 |
下面是另一个例子,查询已发布的文章。你可能会设计成下面的 URL。这个比较特别
1 | GET /articles/release |
查询字符串的写法明显更好
1 | GET /article?release=true |
1 | GET /system/user |
批量操作
批量操作的方法:
- 用逗号分隔放进url里面:http://www.infotech.vip/post/2018,2019
- 将需要删除的一系列id放进请求体里面,但是似乎没有这样的标准(DELETE请求)。当然还可以可以新创建一个POST请求路径带上/batchDelete,内容放在body里面。
方法1:如果删除数据比较多会超过URL长度限制
Url长度限制:
IE7.0 :url最大长度2083个字符,超过最大长度后仍然能提交,但是只能传过去2083个字符。
firefox 3.0.3 :url最大长度7764个字符,超过最大长度后无法提交。
Google Chrome 2.0.168 :url最大长度7713个字符,超过最大长度后无法提交
一次删除上百条记录的情况一般来说比较少,超过长度限制还可以分割成多批次提交
方法2:其实我是不太建议的。因为我们删除操作,肯定使用DELETE请求,但是奈何我们并不建议在DELETE请求里放body体,原因在于:根据RFC标准文档,DELETE请求的body在语义上没有任何意义。事实上一些网关、代理、防火墙在收到DELETE请求后,会把请求的body直接剥离掉,所以如果使用BODY传递参数使用POST请求,路径增加delete关键字区分新建操作
方法3:分成2步完成,第一步发送POST请求,集合所有要删除的IDS,后台创建IDS缓存生成一个key,然后返回key,然后在利用这个key调用DELETE请求
综合考量对于普通系统删除还是把id放在路径上通过逗号分割比较合适,前端限制下传输的ID数量或长度。
扩展:对于删除比如有某个条件限制出现部分条目是不能删除的,一个删除操作也要保证所有成功或者所有都失败,部分失败的情况也应该算失败,后端要先验证是否可以进行删除操作(部分失败的情况你给用户提示成功是不合理的,给用户提示操作失败也是不合理的因为部分成功了,会有歧义,所以比较合理的做法是提前验证保证整个操作可行)。
请求格式
GET/DELETE使用application/x-www-form-urlencoded,POST/PUT使用application/json或multipart/form-data
GET请求参数必须放在URL路径中
POST 参数可以统一使用json格式放在body中
响应格式统一为JSON
状态码
有很多服务器将返回状态码一直设为200,然后在返回body里面自定义一些状态码来表示服务器返回结果的状态码。由于rest api是直接使用的HTTP协议,所以它的状态码也要尽量使用HTTP协议的状态码。
1 | 200 OK 服务器返回用户请求的数据,该操作是幂等的 |
发生错误时
不要返回 200 状态码,一般是状态码反映错误
正确的例子
HTML例子:
1 | GET /token |
扩展:
1 | 批量更新/创建,单个也能用,请求体放多个JSON对象的数组[{},{}]: PUT/POST /device |
SpringMVC、SpringBoot实现例子
1 | /** |
JSON模式
1 | /** |
请求通用内容设计
请求参数尽量不使用嵌套结构,易于解析。请求参数命名规范:
字段 | 说明 |
---|---|
pageSize | 每页显示条数 |
pageNo | 当前页数 |
返回内容命名规范,把返回格式1(部分具体信息跟通用头部放在一起,所以加上resp避免出现同名),返回格式2(通用头部+data具体信息)
字段 | 说明 |
---|---|
respCode/code | 响应码(数值型) |
respMsg/msg | 响应描述 |
resources/data | 返回资源信息集 |
各种URL方案对比
模式一Restful
动词+宾语,URL地址小写字母多个单词使用下划线分隔。?后面跟的参数使用驼峰模式。
Restful的核心思,比如 GET /article
,GET
是动词,article
是宾语。
GET、DELTE用url传递参数,POST、PUT统一用json传递(或者form)参数
1 | GET (查询) /article |
关联关系
1 | POST /user-phone 对于复杂的关联操作可以直接使用数据库中间表的表名(使用层级关系的话不够灵活) |
过滤、排序、字段
1 | 过滤: |
Restful都调用一个后台接口会出现那个接口需要很多if else进行逻辑切换
再列一些例子,有些时候不一定要严格按照Restful规范设计,比如我们数据库设计的时候也有违反数据库范式的时候
1 | ## 反 restful |
最好加上层级关系:对于复杂系统,比如java类名对应一个url层级。对于更复杂的系统还可以增加层级。
比如:
1 | POST /user/login |
模式二传统模式
使用传统URL模式
1 | POST /article/list?id=1 |
模式三Json
使用传统URL数据全部通过json传输
1 | POST /article/list |
返回数据格式一
分页参数与respCode同级
字段 | 上级节点 | 说明 |
---|---|---|
respCode | 响应码(数值型) | |
respMsg | 响应描述 | |
totalCount | 总记录式 | |
hasNextPage | 是否有下一页(非必须) | |
resources | 返回资源信息集 |
返回数据格式二
服务端只返回总条数
字段 | 上级节点 | 说明 |
---|---|---|
code | 响应码(数值型) | |
msg | 响应描述 | |
data | 返回资源信息集 | |
totalCount | data | 总记录式 |
list | data | 具体数据内容 |
返回数据格式三
服务端返回详细分页信息:
字段 | 上级节点 | 说明 |
---|---|---|
code | 响应码(数值型) | |
msg | 响应描述 | |
data | 返回资源信息集 | |
totalCount | data | 总记录式 |
hasNextPage | data | 是否有下一页(非必须) |
pageSize | data | 每页条数 |
totalPage | data | 总页数 |
currPage | data | 当前页号 |
list | data | 具体数据内容 |
返回数据格式四
服务端返回总条数,与最新记录的ID编号。避免查询当天数据,但是数据量实时大量录入,分页的时候引起下一页显示的还是上一页的部分内容,因为分页的时候后台的数据变化了,通过ID固定住分页需要查询的内容。这个方案也不是很好,主要是手机端不做处理的话体验会比较差,这个方案比较适合手机端,触屏拖拉使用,翻页的时候客户端带上lastId参数。
字段 | 上级节点 | 说明 |
---|---|---|
code | 响应码(数值型) | |
msg | 响应描述 | |
data | 返回资源信息集 | |
totalCount | data | 总记录式 |
lastId | data | 最新记录ID |
list | data | 具体数据内容 |
返回错误码设计
json中:code 成功0,失败默认1000或者500(并不是很好与http状态码混淆),快速开发可以先用成功失败两种错误码后续完善。尽可能做到http的状态码不要全部使用200的方式。其他情况可以:
- 复杂点的(不推荐)
1
2
3
4
5
6
7
8A-BB-CCC,6位长度整形int。
A:代表错误级别,1系统级别程序错误,2服务级别业务错误。
BB:代表错误项目或者业务模块号或错误分类,从00开始。
CCC:具体错误编号,自增,从001开始。
比如:权限相关的
2-00-001:客户端appId不存在
2-00-002:token无效或者过期
2-00-003:请求缺少某个必需参数,或者格式不正确 - 不想这么复杂可以直接从501,1000,4000等开始编号,根据具体错误+1顺序编号下去。(推荐1000以上进行错误信息编号)
- 或者 BB-CCC
- 400自定义全局错误客户端可以直接回显信息,40001自定义错误需要客户端处理(不推荐)
- 使用英文作为错误码也是不错的选择,可以直接通过单词知道含义(可以大类加小类AUTH:BAD_PASSWORD,推荐直接错误信息BAD_PASSWORD),阿里云的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27HTTP/1.1 403
x-datahub-request-id: 2018050817492199d6650a00000039
Content-Type: application/json
Content-Length: xxx
{
"ErrorCode": "Unauthorized",
"ErrorMessage": "Authroize failed"
}
# OSS SDK XML
HTTP/1.1 400 Bad Request
x-oss-request-id: 56594298207FB3044385****
Date: Fri, 24 Feb 2012 03:55:00 GMT
Content-Length: 309
Content-Type: text/xml; charset=UTF-8
Connection: keep-alive
Server: AliyunOSS
<Error>
<Code>InvalidArgument</Code>
<Message>no such bucket access control exists</Message>
<RequestId>5***9</RequestId>
<HostId>***-test.example.com</HostId>
<ArgumentName>x-oss-acl</ArgumentName>
<ArgumentValue>error-acl</ArgumentValue>
</Error>
Json层级
对于有层级关系的统一命名用xxxIdList
或者xxxIds
,子集可以是对象数组、也可以是数字数组。
比如用户接口
1 | 提交的时候 |
- 可以roleIds与roleList组合使用,提交的时候使用roleIds查询的时候使用roleList
- 可以统一使用roleList 对象数组,内容统一使用对象
GitHub API
1 | /users/:username/repos |
我们可以看到几个特性:
资源分为单个文档和集合,尽量使用复数来表示资源,单个资源通过添加 id 或者 name 等来表示
一个资源可以有多个不同的 URL
资源可以嵌套,通过类似目录路径的方式来表示,以体现它们之间的关系
不符合 CRUD 的情况
在实际资源操作中,总会有一些不符合 CRUD(Create-Read-Update-Delete) 的情况,一般有几种处理方法。
使用 POST
为需要的动作增加一个 endpoint,使用 POST 来执行动作,比如 POST /resend 重新发送邮件。
增加控制参数
添加动作相关的参数,通过修改参数来控制动作。比如一个博客网站,会有把写好的文章“发布”的功能,可以用上面的 POST /articles/{:id}/publish 方法,也可以在文章中增加 published:boolean 字段,发布的时候就是更新该字段 PUT /articles/{:id}?published=true
把动作转换成资源
把动作转换成可以执行 CRUD 操作的资源, github 就是用了这种方法。
比如“喜欢”一个 gist,就增加一个 /gists/:id/star 子资源,然后对其进行操作:“喜欢”使用PUT /gists/:id/star,“取消喜欢”使用 DELETE /gists/:id/star。
另外一个例子是 Fork,这也是一个动作,但是在 gist 下面增加 forks资源,就能把动作变成 CRUD 兼容的:POST /gists/:id/forks 可以执行用户 fork 的动作。
paypal
路径用-
分隔,内容参数单词用_
分隔
接口设计注意事项
- 接口尽量提供分页功能,可以通过
pageNo=10&pageSize=5
这两个参数进行判断,不传默认查询10条。再定义一个是否分页参数,如果不分页就查询全部paging=false - 对于一类比较通用的功能是使用独立接口还是合并接口通过
type
判断要根据具体情况决定,各有各的好处。独立开来后期可以独立变化互相不影响,但是如果有写修改都是要改的会变成多个接口都要修改,不仅后端要修改,前端可能也需要修改多个调用接口,接口合并他用过type区分后端代码会比较杂。总体上说可能合并接口会好一点。比如:查询设备位置的接口/location?type=1
与/car/location
、/phone/location
多个接口,还有人员携带多种设备/person-device?type=1
与/person-watch
、/person-phone
- 编写API文档是,分类加上
01
两位数编号前缀,具体接口加上[新增]
操作类型前缀,会显得文档比较规范,看过去比较整齐,可能人类对这种带统一格式前缀的文字天生比较敏感
参考
- GitHub REST API v3
- paypal
- paypal2
- A Brief Introduction to REST
- 知乎长文介绍RESTful
- RESTful API Design. Best Practices in a Nutshell.
- 阿里云 NMS
- RESTful 接口实现简明指南
- 阿里云 OSS
- github rest介绍
- RESTful 风格下,一个 Action 需要对多个资源操作要如何理解
- GraphQL一种用于 API 的查询语言
- 微博API
- 网易云错误码设计
- http响应码
- 错误码
- http响应吗RFC
- 服务器端 REST API 常见错误码
- Rest Api
- 阿里云-DataHub错误码设计
- Restful API中的错误处理方法-以及各个平台的错误码