WebAPI 规范
适用对象:需要设计、评审或对接平台 HTTP 接口的前后端开发者。
这页关注的是“接口风格应该怎么统一”,而不是某个具体业务接口长什么样。
什么时候看这页
- 你在新增 WebAPI,想统一 URL、HTTP 动词和响应结构
- 你在评审接口设计,想确认哪些约定是平台推荐口径
- 你在做前后端协作,需要统一状态码和错误返回策略
如果你当前要查的是平台已有对象和上下文,而不是接口规范,请转到 API 参考。
WebAPI 或称 Restful API,是从AJAX发展而来的一种HTTP RPC规范,但是规范的细节大多是建议性的并没有严格标准。不同开发人员理解不一样,所开发出来的接口风格也各不相同,这种差异经常会产生额外的沟通成本,因此我们需要制定一个内部的风格规范来统一内部开发风格,降低前后端协作成本和维护成本。
WebAPI的风格包括:
- HTTP请求部分
- HTTP动词使用
- URL风格
- HTTP请求头
- HTTP响应部分
- HTTP状态码
- HTTP响应头
- 响应体数据格式
理想的情况下应:
- 由 HTTP 动词 表达动作类型
- 由 URL 表达操作的业务对象
- 由 QueryParams 表达操作参数
- 由 请求体 传递请求数据
- 由 HTTP 状态码 表达操作是否成功以及错误类型
- 用 响应体 返回成功后的数据。
但是在实际的业务场景中,HTTP动词数量有限,远不能满足复杂业务的行为动作表达;HTTP状态码和响应体的数据结构中的状态码又有重叠的地方。
因此我们需要区分不同的情况制定更细致的风格指南,指南要体现出以下内容:
- 如何使用 HTTP 动词?
- 如何表达 HTTP 动词无法表达的动作?
- 何时用 HTTP 状态码?
- 何时用响应体中的状态码?
HTTP 请求约定
WebAPI描述
<HTTP动词>:<URL>[QueryParams]
HTTP 动词
动词表
| 动词 | 原始含义 | 使用场景约定 |
|---|---|---|
| GET | 查询、读取记录 | 只读,即不能对业务实体产生副作用 |
| POST | 新建记录 | CRUD中的新建动作,以及自定义业务动作 |
| PUT | 覆盖更新 | 严格来说该方法的效果应该是全量更新,但实际可以与PATCH同义 |
| PATCH | 部分更新 | 部分更新,传什么字段更新什么字段 |
| DELETE | 删除 | 只有表示删除时才能用 |
URL 构成
原则是基本的增删改查接口不要在URL中体现其动作,因为:
- 反模式
- 同义反复不够简洁
- 与关联子实体的url模式类似,会有歧义隐患
风格建议
| API类型 | HTTP动词及URL |
|---|---|
| 列表搜索 | GET /api[/版本号]/<业务资源路径> 不建议使用类似 GET /api[/版本号]/<业务资源路径>/list 的设计 |
| 新建 | POST /api[/版本号]/<业务资源路径> 不建议使用类似 POST /api[/版本号]/<业务资源路径>/create 的设计 |
| 更新 | PUT /api[/版本号]/<业务资 源路径>/<id> 或 PATCH /api[/版本号]/<业务资源路径>/<id> |
| 删除 | DELETE /api[/版本号]/<业务资源路径>/<id> 不建议使用类似 POST /api[/版本号]/<业务资源路径>/delete?id=<id> |
| 获取单条数据 | GET /api[/版本号]/<业务资源路径>/<id> |
| 自定义业务动作 | 推荐:POST /api[/版本号]/<业务资源路径>/<id>/<动作名> 也可以用: POST /api[/版本号]/<业务资源路径>/<动作名>?id=<id> |
| 关联子实体 | GET /api[/版本号]/<业务资源路径>/<id>/<子实体> |
路径组成部分
-
/apiapi前缀,/api表示我们内部使用的应用api,在与外部系统对接的时候我们可以选择用 /open-api 或者 /public-api 来表示不同访问方式的API(其认证方式也会不同) -
[/版本号]API的版本号,非必选,因为我们大部分时候都是前后端一起发版的,前后端API版本不一致的情况不多 -
/<业务资源路径>所谓的业务资源路径就是业务模块的路径,比如 /sys/user,这里有三点规范:- 必须用名词,不论何种业务类型,其业务模块一定是基于其核心业务实体名为模块命名,如果模块名是动词或是形容词,那意味着设计一定是有问题的
- 使用单数形式,模块名到底该用单数还是复数形式,像业务实体和数据表该如何命名的话题一样是存在争议的:
- 支持复数形式 的理由是,复数形式的名词在调用的时候像是一个集合,搜索API就好比是在访问这个集合,取单条数据的API也好比是在这个集合中拿出一个单体实例,语法上比较自然;
- 支持单数形式的理由是,有一些名词的复数形式比较复杂(比如child->children、person->people),对于非英语母语的人来说同时使用复数形式和单数形式的英文单词容易产生混乱,并且在使用了ORM框架的代码中,数据会映射为实体对象,而通常我们会习惯于用单数形式作类名,API的
URL <-> 类名 <-> 表名,这三者就很难统一; - 权衡利弊后,结合我们之前的数据库命名规范和编码规范,还是采用单数形式更合适一些,对于非英语母语的我们大部分英语名词的单数形式更熟悉一些,规范更容易落实。
- 全部使用小写字母,多单词以连字符分隔(烤肉串命名法)使用全小写的原因是网址一般是不区分大小写的,比如主机名(域名)部分是不区分大小写的,路径部分因为windows、linux不同操作系统对路径名的大小写处理方式不一样,传统的web网站有个不成文的约定,就是路径中避免使用大写字母以降低迁移成本,多单词的区隔方式也延续了域名的规则,即用连字符分隔(域名不支持下划线);我们的规范是延续了传统的约定。
-
<id>我们鼓励使用路径参数来传递业务实体的标识,当然这就要求我们的id中最好不要有特殊字符,比如冒号、空格、斜杠等,这会增加我们前端的工作量,即需要对id做url编码。而且这种形式的id设计也是反常规的不利于理解; -
?id=<id>使用Query参数跟路径名和主机名不一样,Query 参数名是大小写敏感的,因为 Query 参数是给后端程序传参的,我们后端程序编码规范中变量名要求用小驼峰命名法,所以 Query 参数名延续程序变量的命名规范,使用小驼峰命名法。 -
/<动作名>除了常规的增删改查接口,一些复杂的业务行为动作可以在id后面或者资源路径名后面增加一个动作名,但其HTTP动词一定是要用POST。注:这里其实是有些反模式的,但是如果严格按照RESTful API的建议设计,有些接口使用起来会变得很复杂;关于这个问题Google Cloud API的方案比较激进,它是用:<动作名>来表达自定义方法的,这个方案是对传统URL做了扩展,虽然使API看起来更规整了,但也增加了后端API路由的复杂度 -
/<子实体>有些时候我们会有一些关联查询设计,比如查询部门下面的所有员工: GET /api/department/1/employee,子实体的命名与业务资源路径的命名规范一样,必须用名词的单数形式,多单词以连字符分隔,有时不是子实体,而是实体的部分信息比如: GET /api/task/20230001/status,即单独查询某个任务状态 ;但是不建议做多层次嵌套设计,如:GET /api/department/1/employee/1001/position,实践表明这种设计会同时增加前端和后端的程序的复杂度,实用性很低,但是沟通成本和维护成本很高。
HTTP 请求头
一般来说,如非必要不要通过HTTP请求头来传递业务参数,因为这种参数调试起来比较麻烦。
基于 token 的身份认证需要传 Authorization 头,但这部分对于业务功能的 API 设计是透明的,因此在此不展开描述。
HTTP 响应约定
HTTP 状态码
| 状态码 | 含义 | 响应内容 |
|---|---|---|
| 200 | 成功返回 | 无要求 |
| 201 | 已创建 | HTTP协议规范中要求不能有响应体 响应头中需要有Location字段提供创建的资源的 URL |
| 202 | 服务器已接受请求但处理未完成 | 无要求 |
| 204 | 请求已处理,如 DELETE 请求处理成功 | HTTP协议规范中要求不能有响应体 |
| 400 | 请求存在于法错误或者参数格式错误 | 无要求 |
| 401 | 未认证,如未提供token或者token已过期导致无法确定请求者的身份 | 无要求 |
| 403 | 无权限,token有效,但当前身份没有权限访问该接口 | 无要求 |
| 404 | 请求资源未找到 | 无要求 |
| 410 | 记录已不存在且不可恢复 与404的区别是,410暗示了该资源可能存在过,但是已经被永久删除且不可恢复,也可能是该URL的标识不合法,比如在黑名单内等情况,永远不可能创建此资源 | 无要求 |
| 422 | 提交数据格式不符合业务逻辑、缺少必要参数等 与400错误的区别是,这里的数据格式形式上是可识别的,但是数 据逻辑不正确,比如向一个新建用户的接口提交了部门信息,或者缺少必要字段,比如缺少用户名等信息 | 无要求 |
| 500 | 服务器错误,如代码错误、数据库服务器错误、内存不足等 | 无要求 |
| 502 | 上游服务器错误,一般是Nginx或Gateway调用上游服务器时,上游服务器宕机或未启动,由Nginx或者Gateway返回错误。 一般情况下,我们的业务服务不应返回此类状态码。 | 无要求 |
| 503 | 服务不可用,如服务器超载、服务器刷新未完成等 一般情况下,我们的业务服务不应返回此类状态码。 我们框架内会有一个过滤器防止低代码应用上下文正在刷新期间处理请求,这时候会返回一个503错误。 | 无要求 |
| 504 | 上游服务器响应超时,一般是Nginx或Gateway调用上游服务器时,上游服务器处理时间过长,由Nginx或者Gateway返回错误。 一般情况下,我们的业务服务不应返回此类状态码。 | 无要求 |
基于以上HTTP协议规范,我们约定以上状态码的使用场景如下:
| 状态码 | 使用场景 |
|---|---|
| 200 | 大多数情况下都要返回200状态码 |
| 201 | 可以在创建数据记录的时候返回201,但不强制要求使用,使用的时候不能有响应体,因此需要前端的请求处理工具库做好兼容性处理。 |
| 202 | 可以在执行异步任务的时候返回202,但不强制要求。 |
| 204 | 可以在执行删除操作的时候返回204,但不强制要求使用,使用的时候不能有响应体,因此需要前端的请求处理工具库做好兼容性处理 |
| 400 | 业务代码不应返回400错误,此类错误应由后端的Web框架自动返回。 |
| 401 | 此状态码应由安全框架返回,业务代码不应返回401代码,尤其需要注意的是登录接口当用户名密码校验失败的时候不应返回401错误,而应返回200状态码(因为登录接口应当允许匿名访问,认证不通过只是凭证有问题,属于登录逻辑的一部分) |
| 403 | 此状态码大部分情况下由安全框架返回,除非在业务代码中有自定义数据权限逻辑。 |
| 404 | 当尝试访问指定ID数据不存在的时候建议返回404,但是查询列表数据为空时不能使用此状态码。 |
| 410 | 一般来说不强制要求使用此状态码,大多数情况下使用404足以,但是如果有需要跟404区分的场景下也可以用此状态码。 |
| 422 | 推荐在接口的数据校验部分使用此状态码返回错误信息,但不强制要求。 |
| 500 | 未处理的运行时异常返回500状态码,调试环境下可以同时返回运行栈信息,生产环境下不能暴露异常细节 |
| 502 | 由反向代理服务器或API网关返回,不能由业务代码返回 |
| 503 | 由应用容器或全局过滤器返回,不能由业务代码返回 |
| 504 | 由反向代理服务器或API网关返回,不能由业务代码返回 |
何时使用4XX、500状态码?何时使用响应体结构中的code?
一般来讲,业务代码中通常不需要主动输出500状态的响应,500状态码是由异常引发的。
4XX状态码通常是针对形式性检查的:
- 400状态码:如接口期望的是 application/json 格式的请求,实际传入了 multipart/form-data 格式的请求,或者传入的JSON数据存在语法错误。
- 422状态码:如接口期待传入的数据中要有 gender 字段,且这个 gender 字段取值应当是 "男" 或 "女",但实际传入了数字1,这时候可以返回422。
而有些时候,尤其是会触发业务流程变化的操作(创建/修改/删除/自定义业务操作)需要结合其他业务信息或状态检查关联逻辑的时候(比如,创建出库单时物料库存不足),提交的数据形式检查没有问题,但是因为关联逻辑限制不能允许此业务发生,就需要返回200响应,并通过响应体中的code和msg说明错误原因。
HTTP 响应头
同 HTTP 请求头,不推荐对响应头做特别设计。
响应体结构
| 字段 | 含义 | 使用场景约定 |
|---|---|---|
| code | 响应代码 | 0代表成功 -1代表自定义错误 其他具体数字需要有对应的错误代码表 |
| msg | 错误信息 | 一般来说只有错误的时候才需要返回,但成功的时候也可以返回,需要与前端约定 |
| data | 返回数据 | 一般要求必须是键值对(即JSON中的Object),如需返回一个列表,也建议以items为key,而不是直接返回一个数组 |
参考资料
- Microsoft REST API Guidelines
- Best practices for REST API design
- RESTful API Desing 13 Best Practices to Make Your Users Happy
- The Web API Checklist -- 43 Things To Think About When Designing, Testing, and Releasing your API
- RESTful API 最佳实践
- Google Cloud API 设计指南