http API RESTful接口设计原则

REST全称是Representational State Transfer,中文意思是表述(通常译为表征)状态转移。
如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构,下面介绍RESTful API接口设计原则

接口设计具体还是要根据需求来定义,需要保证原子性的情况比较多,比如设计几个操作一部分成功一部分失败应该统一定义成失败,把成功的部分回滚还可以者先处理失败概率比较大的操作再处理失败概率小的操作。

URL设计

URL地址

小写字母多个单词使用下划线分隔。?后面跟的参数使用驼峰模式

动词+宾语

Restful的核心思,比如 GET /articleGET是动词,article是宾语。

1
2
3
4
5
GET (read)
POST (create)
PUT (update)
PATCH (update) 一般不用
DELETE (delete)

动词覆盖

有些客户端只能使用GET和POST这两种方法。服务器必须接受POST模拟其他三个方法(PUT、PATCH、DELETE)。
这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override属性,告诉服务器应该使用哪一个动词,覆盖POST方法。

1
2
POST /api/article/4 HTTP/1.1  
X-HTTP-Method-Override: PUT

上面代码中,X-HTTP-Method-Override指定本次请求的方法是PUT,而不是POST。

宾语必须是名词

宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/article这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的:

1
2
3
/get_all_article
/create_new_article
/delete_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
2
3
4
GET /system/user
也建议简写成:
GET /user
实际使用的时候根据具体情况定义,如果接口特别多还是可以增加一个分类比较好理解。

批量操作

批量操作的方法:

  1. 用逗号分隔放进url里面:http://www.infotech.vip/post/2018,2019
  2. 将需要删除的一系列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
2
3
4
5
6
7
8
9
200 OK 服务器返回用户请求的数据,该操作是幂等的
201 CREATED 新建或者修改数据成功
204 NOT CONTENT 删除数据成功
400 BAD REQUEST 用户发出的请求有问题,该操作是幂等的
401 Unauthoried 表示用户没有认证,无法进行操作
403 Forbidden 用户访问是被禁止的,没有权限
422 Unprocesable Entity 当创建一个对象时,发生一个验证错误
500 INTERNAL SERVER ERROR 服务器内部错误,用户将无法判断发出的请求是否成功
503 Service Unavailable 服务不可用状态,多半是因为服务器问题,例如CPU占用率大

发生错误时

不要返回 200 状态码,一般是状态码反映错误

正确的例子

HTML例子:

1
2
3
4
GET /token
GET /device/{id}
DELETE /device/{id}
GET /author/12?categorie=2

扩展:

1
2
3
批量更新/创建,单个也能用,请求体放多个JSON对象的数组[{},{}]: PUT/POST /device  
批量查询/删除: GET/DELETE /device/{id1},{id2}
扩展搜索资源,路径增加'search'关键字:get /search/device?q=keyword

SpringMVC、SpringBoot实现例子

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
27
28
29
30
/**
* 分页查询
* http://127.0.0.1:8081/infotech/api/user?pageNo=1&pageSize=5&userName=a
*/
@GetMapping("/user")
public Result page(@RequestParam Map<String, Object > form) {
Page list = userService.page(form);
return Result.ok().put("resources", list);
}
/**
* 更新
* http://127.0.0.1:8081/infotech/api/user?account=1&userId=99999
* http://127.0.0.1:8081/infotech/api/user
* BODY account=1&userId=99999
*/
@PostMapping("/user")
public Result update(@RequestParam HashMap<String, Object > form) {
userService.update(form);
return Result.ok();
}
/**
* 删除多个id
* http://127.0.0.1:8081/infotech/api/user?1,2
*/
@DeleteMapping("/user/{id}")
public Result delete(@PathVariable("id") String[] ids) {
userService.delete(ids);
return Result.ok();
}

JSON模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 更新
* http://127.0.0.1:8081/infotech/api/user
* BODY JSON
* {
* "account" : 1,
* "userId" : 99999
* }
*/
@PostMapping("/user")
public Result update(@RequestBody HashMap<String, Object > form) {
userService.update(form);
return Result.ok();
}

请求通用内容设计

请求参数尽量不使用嵌套结构,易于解析。请求参数命名规范:

字段 说明
pageSize 每页显示条数
pageNo 当前页数

返回内容命名规范,把返回格式1(部分具体信息跟通用头部放在一起,所以加上resp避免出现同名),返回格式2(通用头部+data具体信息)

字段 说明
respCode/code 响应码(数值型)
respMsg/msg 响应描述
resources/data 返回资源信息集

各种URL方案对比

模式一Restful

动词+宾语,URL地址小写字母多个单词使用下划线分隔。?后面跟的参数使用驼峰模式。
Restful的核心思,比如 GET /articleGET是动词,article是宾语。
GET、DELTE用url传递参数,POST、PUT统一用json传递(或者form)参数

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
GET (查询) /article
GET (查询) /article/1
POST (新增) /article
PUT (更新) /article/1 更新也可以用不需要ID的形式/article,更新的json内容中包含ID
DELETE (删除) /article/1

// 复杂情况
POST /{version}/{namespace}/{action} 复杂操作
POST /{version}/{namespace}/{search-resource} 复杂查询 POST /v1/factory/widgets-search
POST (复杂查询方案,通过json传递参数) /query/article
GET /article/XX?xx=xx
GET /article?pageSize=10
GET /article/${id}?type=xx
POST (复杂查询方案,通过json传递参数) /query/aritcle/comment
POST (复杂查询方案,通过json传递参数) /query/aritcle/type=xx
GET /v1/article?[&keyword=xxx][&enable=1][&pageNo=10][&pageSize=20]
// 各个名词有多种用途或者多种复杂查询 要保证id不要跟其他关键字组成的路径冲突(比如id也叫track就会有问题),给id加前缀或者使用数字或者使用UUID等固定长度的id
GET /cell/${id}
GET /cell/track imsi轨迹
GET /cell/companion imsi伴随
GET /cell/recent 最近数据
GET /cell-companion (或者)imsi伴随这种比较符合restful规范
POST /query/cell/companion (或者)imsi伴随
GET /orgs/octokit/repos
GET /repos/vmg/redcarpet/issues?state=closed

关联关系

1
2
3
POST /user-phone 对于复杂的关联操作可以直接使用数据库中间表的表名(使用层级关系的话不够灵活)
POST /user/1/phone/2 这种属于层级关系url太长不建议使用
POST /user?type=phone 通过?加上type进行区分的方式也不建议使用,会导致后端太多判断语句

过滤、排序、字段

1
2
3
4
5
6
7
8
9
10
11
过滤:
GET /car?color=red
GET /car?seats<=2
排序:
GET /car?sort=-manufactorer,+model
字段选择,比如select下拉框:
GET /car?fields=manufacturer,model,id,color
分页:
GET /cars?pageNo=10&pageSize=5
版本:
/v1/

Restful都调用一个后台接口会出现那个接口需要很多if else进行逻辑切换

再列一些例子,有些时候不一定要严格按照Restful规范设计,比如我们数据库设计的时候也有违反数据库范式的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 反 restful
post: `/orders/checkout`
## restful 风格
post: `/checkouts`

UPDATE /profile/primaryAddress/city
GET /v1/customer/partners/{partner_id}/merchant-integrations/{merchant_id} // Shows status information for sellers that the partner on-boards, by partner ID
POST /v1/customer/partner-referrals // Creates a partner referral

get /product/manufacturer/{manufacturer_id}/mac // 根据厂家获取当前最新产品mac
get /product/manufacturer/{manufacturer_id}/count // 根据厂家查询总生成产品MAC数量

POST /user/login
POST /user/logout
转换为
POST/DELETE /auth

修改密码
POST /user/{id}/pwd
POST /user/pwd/{id}

微信的登录例子 http请求方式: GET
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

最好加上层级关系:对于复杂系统,比如java类名对应一个url层级。对于更复杂的系统还可以增加层级。
比如:

1
2
3
POST /user/login
POST /user/logout
放在类名增加user注解。增删改查可以不加路径注解,自定义的路径login logout增加注解。这样批量查询也就不能使用`query/user`这种形式了,使用`user/query`。个人建议用这种形式

模式二传统模式

使用传统URL模式

1
2
3
4
POST /article/list?id=1
POST /article/add
POST /article/update
POST /article/delete?id=1,2

模式三Json

使用传统URL数据全部通过json传输

1
2
3
4
POST /article/list
POST /article/add
POST /article/update
POST /article/delete

返回数据格式一

分页参数与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. 复杂点的(不推荐)
    1
    2
    3
    4
    5
    6
    7
    8
    A-BB-CCC,6位长度整形int。
    A:代表错误级别,1系统级别程序错误,2服务级别业务错误。
    BB:代表错误项目或者业务模块号或错误分类,从00开始。
    CCC:具体错误编号,自增,从001开始。
    比如:权限相关的
    2-00-001:客户端appId不存在
    2-00-002:token无效或者过期
    2-00-003:请求缺少某个必需参数,或者格式不正确
  2. 不想这么复杂可以直接从501,1000,4000等开始编号,根据具体错误+1顺序编号下去。(推荐1000以上进行错误信息编号)
  3. 或者 BB-CCC
  4. 400自定义全局错误客户端可以直接回显信息,40001自定义错误需要客户端处理(不推荐)
  5. 使用英文作为错误码也是不错的选择,可以直接通过单词知道含义(可以大类加小类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
    27
    HTTP/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

    <?xml version="1.0" encoding="UTF-8"?>
    <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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 提交的时候
- phoneList Array
- phoneNo 手机号
- network 运营商
- roleIds Array 直接字符串数组
- roleList Array 直接角色列表
- id 角色ID
# 查询的时候
- phoneList Array
- phoneNo 手机号
- network 运营商
- id
- roleList Array 角色列表
- id 角色ID
- name 角色名称
    1. 可以roleIds与roleList组合使用,提交的时候使用roleIds查询的时候使用roleList
    1. 可以统一使用roleList 对象数组,内容统一使用对象

GitHub API

1
2
3
4
5
/users/:username/repos
/users/:org/repos
/repos/:owner/:repo
/repos/:owner/:repo/tags
/repos/:owner/:repo/branches/:branch

我们可以看到几个特性:

资源分为单个文档和集合,尽量使用复数来表示资源,单个资源通过添加 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两位数编号前缀,具体接口加上[新增]操作类型前缀,会显得文档比较规范,看过去比较整齐,可能人类对这种带统一格式前缀的文字天生比较敏感

参考