微服务应用 API 设计规范
遵循统一的 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": {
"list": [],
"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": []
}
}