微服务应用 API 设计规范
本规范是笔者为所带领的研发团队制定的 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]}
资源 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 参数传递
对于 POST 请求不要使用 Query 参数,非资源 ID 参数只能通过请求体传递。
业务操作放到路径里
POST /user/sendVerifyCode
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 // 业务数据
}
- 业务错误码采用常量字符串格式(只能使用大写字母、下划线和数字),形如
USER__USER__USERNAME_DUPLICATED
,其中前两级依次为服务和模块,一些与服务和模块无关的公共错误码没有前缀; - 业务错误描述可直接展示给用户(但不推荐),请勿包含任何涉密信息,考虑到国际化,请使用英文;
- 业务数据如果只有单项那么直接通过
data
字段返回,这样可以让后端省去大量的包装类定义,如果有多项那么只能再包一层,在data
下通过不同字段区分,如果没有则为null
; - 只有在 HTTP 响应状态码为
200
时才保证响应体符合标准格式;
示例
单项业务数据直接通过 data
字段返回:
{
"code": "OK",
"message": "",
"data": {
"id": 1,
"username": "jack"
}
}
多项业务数据需包一层:
{
"code": "OK",
"message": "",
"data": {
"user": {
"id": 1,
"username": "jack"
},
"roles": []
}
}
列表数据:
{
"code": "OK",
"message": "",
"data": {
"users": [],
"total": 1000
}
}
空列表如果属于正常情况,那么请返回空数组而不是 null,这样可以避免调用方去做 null 判断。 业务出错:
{
"code": "BILLING__PAY__MONEY_NOT_ENOUGH",
"message": "money not enough",
"data": null
}
分页规范
基于页
适合客户端有翻页条,按页展示数据,数据集变化较慢的场景。
- 分别使用
total
、pageNo
、pageSize
、list
来传递和返回总数(可选)、当前页码、每页条数和当页数据; - 冗余返回当前页码和每页条数,以便客户端依据响应结果即可知道如何请求下页数据;
{
"code": "ok",
"message": "",
"data": {
"total": 100,
"pageNo": 1,
"pageSize": 10,
"list": []
}
}
基于偏移量
适合没有翻页条的流式加载模式场景,如果起始位置使用主键 ID、创建时间这样的排序字段,服务端可以通过大于或等于比较来加速查询,并且在数据集有变化时能够保证分批获取的数据不重复。
- 分别使用
total
、offset
、limit
、list
来传递和返回总数(可选)、起始位置、返回条数和当页数据; - 起始位置除了是位置序号,还可以是任意有序字段的值,比如创建时间;
- 冗余返回起始位置和返回条数,以便客户端依据响应结果即可知道如何请求下页数据;
{
"code": "ok",
"message": "",
"data": {
"total": 100,
"offset": 0,
"limit": 10,
"list": []
}
}