本规范是笔者为所带领的研发团队制定的 API 设计规范,从请求方式、请求路径、请求参数、请求体、响应状态码、响应体等方面对 API 的设计进行了一些约定,以便统一各 API 的风格。
命名规范
如无特别说明,路径、参数、变量等命名统一采用首字母小写的驼峰方式。
字符编码
如无特别说明,字符编码统一采用 UTF-8。
URL 规范
请求方式
GET 读取
POST 创建,非幂等
PUT 整体替换,幂等
PATCH 部分更新
DELETE 删除
为了简化沟通和理解,如非必要请勿使用 PUT、PATCH 和 DELETE,这三种请求方式均可使用 POST 结合路径中的操作来实现,具体可参考下面的“路径和参数”章节。
路径和参数
围绕资源来操作
GET /user/list 列表查询
GET /user/info/1 ID 查询,如需查询多个,ID 列表通过 Query 参数传递,形如 ids=1,2,3
POST /user/create 创建
POST /user/update/1 更新,只更新传递的字段,未传递的字段保持不变
POST /user/replace/1 替换,整体更新,未传递的字段将清空
POST /user/delete/1 删除,如需删除多个,ID 列表通过请求体传递,形如 {"ids": [1, 2, 3]}
Plain
Copy
资源 ID 参数通过路径参数来传递。考虑到有些业务操作不是针对资源,比如发送通知或邮件,路径前缀不要使用 users
这样的复数形式,建议使用模块名。
非资源 ID 参数放到 Query 里
GET /user/list?gender=male&minAge=18
GET /user/infoByUsername?username=jack
GET /user/follow?followingId=1&followerId=2 涉及到多个 ID 组合才能定位资源时,统一通过 Query 参数传递
Plain
Copy
对于 POST 请求不要使用 Query 参数,非资源 ID 参数只能通过请求体传递。
业务操作放到路径里
POST /user/sendVerifyCode
Plain
Copy
API 路径
对于采用微服务架构的应用,需要通过路径前缀来区分前后端资源,以及不同的微服务。
所有微服务的 API 统一到 /api
路径前缀下,可在反向代理层添加该前缀,在传递给后端服务时抹去该前缀,以便该前缀对后端服务来说无感知;
在应用网关层为各微服务添加相应的路径前缀(比如用户微服务 /user
),该前缀同样对各微服务来说无感知;
在微服务内部按模块划分 API,比如用户微服务的用户模块的创建用户 API /user/create
(对外暴露的完整路径为 /api/user/user/create
,这里服务名和模块名同名);
API 版本
对于内部使用的 API,自己可以控制客户端升级节奏,应避免使用 API 版本,因为维护多版本 API 的成本很高。对于对外开放的 API,由于无法控制使用方客户端升级节奏,那么可以通过多版本 API 来实现平滑升级,在废弃老版 API 之前给使用方留足够的升级时间。 API 版本号可在不同的层级上添加,以前面的创建用户 API /api/user/user/create
为例,按作用范围由大到小有以下几种方式:
/api/v1/user/user/create
在应用网关层级添加(推荐);
/api/user/v1/user/create
在微服务层级添加;
/api/user/user/v1/create
在模块层级添加;
/api/user/user/create/v1
在 API 层级添加;
不建议为不同版本的服务启动不同的实例,随着版本的不断升级,后期的维护工作会越来越大。对于同一个服务的不同版本 API,应使用一套代码,新版 API Controller 可以通过继承老版 Controller 来尽量复用现有代码。
请求
公共参数
通过 HTTP 头来传递公共参数,优先使用 HTTP 标准头,没有合适的再自定义,自定义 HTTP 头需以 X-
打头。
HTTP 头 | 示例 | 用途 |
Authorization | Bearer <token> | 认证服务颁发的 Token |
Accept-Language | zh-CN,zh;q=0.9,en-US,en;q=0.1 | 可接受的响应内容语言,优先使用权重高的 |
X-Client-Name | BasicAI | 客户端名称 |
X-Client-Version | v1.0.0 | 客户端版本 |
请求体
请求体采用 JSON 格式,且为一个 JSON 对象;
请求体为单个值时需包装为一个对象,以保持最外层结构统一;
POST /user/create
{
"username": "jack",
"password": "12345678",
"age": 17}
响应
响应状态码
HTTP 状态码设计的初衷是用于静态资源访问场景,对于 API 这种动态服务场景,许多状态码都不适用,或者根本无法表示各种千奇八怪的业务错误。因此这里推荐只使用下面这些少量的 HTTP 状态码,其它业务异常情况统一响应 200
状态码,并在响应体里通过 code
返回具体的业务错误码。
200 OK 操作成功
400 Bad Request 一般性客户端错误
401 Unauthorized 未认证
403 Forbidden 未授权
404 Not Found 资源未找到
405 Method Not Allowed 请求方式不支持
429 Too Many Requests 请求太频繁
500 Internal Server Error 服务器内部错误
502 Bad Gateway 网关请求上游服务时出错
503 Service Unavailable 服务不可用,比如重启或维护中
504 Gateway Timeout 网关请求上游服务超时
响应体
响应体为 JSON 对象,结构如下:
{
"code": "OK", // 业务错误码 "message": "", // 业务错误描述 "data": null // 业务数据}
JSON
Copy
业务错误码采用常量字符串格式(只能使用大写字母、下划线和数字),形如 USER__USER__USERNAME_DUPLICATED
,其中前两级依次为服务和模块,一些与服务和模块无关的公共错误码没有前缀;
业务错误描述可直接展示给用户(但不推荐),请勿包含任何涉密信息,考虑到国际化,请使用英文;
业务数据如果只有单项那么直接通过 data
字段返回,这样可以让后端省去大量的包装类定义,如果有多项那么只能再包一层,在 data
下通过不同字段区分,如果没有则为 null
;
只有在 HTTP 响应状态码为 200
时才保证响应体符合标准格式;
示例
单项业务数据直接通过 data
字段返回:
{
"code": "OK",
"message": "",
"data": {
"id": 1,
"username": "jack"
}}
JSON
Copy
多项业务数据需包一层:
{
"code": "OK",
"message": "",
"data": {
"user": {
"id": 1,
"username": "jack"
},
"roles": []
}}
JSON
Copy
列表数据:
{
"code": "OK",
"message": "",
"data": {
"users": [],
"total": 1000
}}
JSON
Copy
空列表如果属于正常情况,那么请返回空数组而不是 null,这样可以避免调用方去做 null 判断。 业务出错:
{
"code": "BILLING__PAY__MONEY_NOT_ENOUGH",
"message": "money not enough",
"data": null}
JSON
Copy
分页规范
基于页
适合客户端有翻页条,按页展示数据,数据集变化较慢的场景。
分别使用 total
、pageNo
、pageSize
、list
来传递和返回总数(可选)、当前页码、每页条数和当页数据;
冗余返回当前页码和每页条数,以便客户端依据响应结果即可知道如何请求下页数据;
{
"code": "ok",
"message": "",
"data": {
"total": 100,
"pageNo": 1,
"pageSize": 10,
"list": []
}}
JSON
Copy
基于偏移量
适合没有翻页条的流式加载模式场景,如果起始位置使用主键 ID、创建时间这样的排序字段,服务端可以通过大于或等于比较来加速查询,并且在数据集有变化时能够保证分批获取的数据不重复。
分别使用 total
、offset
、limit
、list
来传递和返回总数(可选)、起始位置、返回条数和当页数据;
起始位置除了是位置序号,还可以是任意有序字段的值,比如创建时间;
冗余返回起始位置和返回条数,以便客户端依据响应结果即可知道如何请求下页数据;
{
"code": "ok",
"message": "",
"data": {
"total": 100,
"offset": 0,
"limit": 10,
"list": []
}}
本文作者:CIO之家的朋友 来源:jaggerwang
CIO之家 www.ciozj.com 微信公众号:imciow