Skip to content

微服务应用 API 设计规范

Cover

遵循统一的 API 规范对 API 使用者更友好,同时也能降低 API 管理的成本,本文描述了一些 API 设计规范,其中大部分是通用的,只有少数特定于微服务架构应用。

命名规范

如无特别说明,路径、参数、变量等命名统一采用首字母小写的驼峰方式。

字符编码

如无特别说明,字符编码统一采用 UTF-8。

URL 规范

请求方式

  1. GET 读取
  2. POST 创建,非幂等
  3. PUT 整体替换,幂等
  4. PATCH 部分更新
  5. DELETE 删除

为了简化沟通和理解,如非必要请勿使用 PUT、PATCH 和 DELETE,这三种请求方式均可使用 POST 结合路径中的操作来实现,具体可参考下面的“路径和参数”章节。

路径和参数

围绕资源来操作

txt
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 里

txt
GET /user/list?gender=male&minAge=18
GET /user/infoByUsername?username=jack
GET /user/follow?followingId=1&followerId=2 涉及到多个 ID 组合才能定位资源时,统一通过 Query 参数传递

对于 POST 请求不要使用 Query 参数,非资源 ID 参数只能通过请求体传递。

业务操作放到路径里

txt
POST /user/sendVerifyCode

API 路径

对于采用微服务架构的应用,需要通过路径前缀来区分前后端资源,以及不同的微服务。

  1. 所有微服务的 API 统一到 /api 路径前缀下,可在反向代理层添加该前缀,在传递给后端服务时抹去该前缀,以便该前缀对后端服务来说无感知;
  2. 在应用网关层为各微服务添加相应的路径前缀(比如用户微服务 /user),该前缀同样对各微服务来说无感知;
  3. 在微服务内部按模块划分 API,比如用户微服务的用户模块的创建用户 API /user/create(对外暴露的完整路径为 /api/user/user/create,这里服务名和模块名同名);

API 版本

对于内部使用的 API,自己可以控制客户端升级节奏,应避免使用 API 版本,因为维护多版本 API 的成本很高。对于对外开放的 API,由于无法控制使用方客户端升级节奏,那么可以通过多版本 API 来实现平滑升级,在废弃老版 API 之前给使用方留足够的升级时间。

API 版本号可在不同的层级上添加,以前面的创建用户 API /api/user/user/create 为例,按作用范围由大到小有以下几种方式:

  1. /api/v1/user/user/create 在应用网关层级添加(推荐);
  2. /api/user/v1/user/create 在微服务层级添加;
  3. /api/user/user/v1/create 在模块层级添加;
  4. /api/user/user/create/v1 在 API 层级添加;

不建议为不同版本的服务启动不同的实例,随着版本的不断升级,后期的维护工作会越来越大。对于同一个服务的不同版本 API,应使用一套代码,新版 API Controller 可以通过继承老版 Controller 来尽量复用现有代码。

请求

公共参数

通过 HTTP 头来传递公共参数,优先使用 HTTP 标准头,没有合适的再自定义,自定义 HTTP 头需以 X- 打头。

HTTP 头示例用途
AuthorizationBearer <token>认证服务颁发的 Token
Accept-Languagezh-CN,zh;q=0.9,en-US,en;q=0.1可接受的响应内容语言,优先使用权重高的
X-Client-NameBasicAI客户端名称
X-Client-Versionv1.0.0客户端版本

请求体

  1. 请求体采用 JSON 格式,且为一个 JSON 对象;
  2. 请求体为单个值时需包装为一个对象,以保持最外层结构统一;

POST /user/create

json
{
  "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 对象,结构如下:

json
{
  "code": "OK", // 业务错误码
  "message": "", // 业务错误描述
  "data": null // 业务数据
}
  1. 业务错误码采用常量字符串格式(只能使用大写字母、下划线和数字),形如 USER__USER__USERNAME_DUPLICATED,其中前两级依次为服务和模块,一些与服务和模块无关的公共错误码没有前缀;
  2. 业务错误描述可直接展示给用户(但不推荐),请勿包含任何涉密信息,考虑到国际化,请使用英文;
  3. 业务数据如果只有单项那么直接通过 data 字段返回,这样可以让后端省去大量的包装类定义,如果有多项那么只能再包一层,在 data 下通过不同字段区分,如果没有则为 null;
  4. 只有在 HTTP 响应状态码为 200 时才保证响应体符合标准格式;

单项业务数据直接通过 data 字段返回示例:

json
{
  "code": "OK",
  "message": "",
  "data": {
    "id": 1,
    "username": "jack"
  }
}

多项业务数据需包一层示例:

json
{
  "code": "OK",
  "message": "",
  "data": {
    "user": {
      "id": 1,
      "username": "jack"
    },
    "roles": []
  }
}

列表数据示例:

json
{
  "code": "OK",
  "message": "",
  "data": {
    "list": [],
    "total": 1000
  }
}

空列表如果属于正常情况,那么请返回空数组而不是 null,这样可以避免调用方去做 null 判断。

业务出错示例:

json
{
  "code": "BILLING__PAY__MONEY_NOT_ENOUGH",
  "message": "money not enough",
  "data": null
}

分页规范

基于页

适合客户端有翻页条,按页展示数据,数据集变化较慢的场景。

  1. 分别使用 total、pageNo、pageSize、list 来传递和返回总数(可选)、当前页码、每页条数和当页数据;
  2. 冗余返回当前页码和每页条数,以便客户端依据响应结果即可知道如何请求下页数据;
json
{
  "code": "ok",
  "message": "",
  "data": {
    "total": 100,
    "pageNo": 1,
    "pageSize": 10,
    "list": []
  }
}

基于偏移量

适合没有翻页条的流式加载模式场景,如果起始位置使用主键 ID、创建时间这样的排序字段,服务端可以通过大于或等于比较来加速查询,并且在数据集有变化时能够保证分批获取的数据不重复。

  1. 分别使用 total、offset、limit、list 来传递和返回总数(可选)、起始位置、返回条数和当页数据;
  2. 起始位置除了是位置序号,还可以是任意有序字段的值,比如创建时间;
  3. 冗余返回起始位置和返回条数,以便客户端依据响应结果即可知道如何请求下页数据;
json
{
  "code": "ok",
  "message": "",
  "data": {
    "total": 100,
    "offset": 0,
    "limit": 10,
    "list": []
  }
}

参考资料

  1. REST 十诫
  2. 使用 OpenAPI 构建 API 文档