3. 请求与响应建模:请求方法、状态码与错误结构
接口建模这件事,说白了就是定规矩。你想想看,前后端各干各的,如果没有一套清晰的请求和响应规范,联调的时候就是灾难现场。我个人习惯在项目启动的第一周,就把请求方法怎么用、状态码怎么传、错误长什么样,全部写进接口文档里。这活儿看着琐碎,但能省掉后面80%的扯皮时间。
3.1 请求方法:不只是GET和POST
很多团队做接口,从头到尾只用GET和POST。我见过一个项目,删除资源也用POST,传一个action=delete的参数。嗯,这其实是在给自己挖坑。RESTful风格里,每种方法都有明确的语义,用对了,接口就是自文档化的。
| 方法 | 语义 | 幂等 | 安全 | 请求体 |
|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 无 |
| POST | 创建资源 / 触发操作 | 否 | 否 | 有 |
| PUT | 全量替换资源 | 是 | 否 | 有 |
| PATCH | 部分更新资源 | 否 | 否 | 有 |
| DELETE | 删除资源 | 是 | 否 | 通常无 |
核心原则:GET和DELETE是幂等的,调用一次和调用一百次,资源状态不变。POST和PATCH不是幂等的,重复提交可能产生多条记录。我遇到过有人用POST做查询,结果网关重试时生成了两条订单——这就是方法选错引发的血案。
3.2 状态码设计:别只用200和500
状态码是服务端给客户端的「第一张脸」。我见过最省事的做法:所有请求都返回200,然后在body里塞一个code字段表示成功或失败。这样做,客户端就得先解析body,再判断业务状态。你想想看,中间件、网关、负载均衡器,它们只看HTTP状态码,不看body。所以,HTTP状态码必须准确表达传输层的语义。
3.2.1 成功类(2xx)
- 200 OK:GET、PUT、PATCH成功时返回。我习惯在查询列表时也用200,哪怕结果是空数组。
- 201 Created:POST创建资源成功后返回。记得在响应头里带上
Location字段,指向新资源的URL。 - 204 No Content:DELETE成功时返回。没有body,干净利落。
3.2.2 客户端错误(4xx)
- 400 Bad Request:参数校验失败、JSON格式错误。别把参数缺失也返回500,那是客户端的问题。
- 401 Unauthorized:未认证,比如没带token或token过期。
- 403 Forbidden:已认证但没权限。我曾经在一个权限系统里,把401和403混着用,结果前端不知道是该重新登录还是弹「无权限」——后来我定了个规矩:401让用户滚去登录,403让用户找管理员。
- 404 Not Found:资源不存在。注意,不要暴露「是路径不对还是ID不存在」,防止信息泄露。
- 409 Conflict:资源冲突,比如重复创建、版本号不一致。
- 422 Unprocessable Entity:语义错误,比如邮箱格式不对、库存不足。
3.2.3 服务端错误(5xx)
- 500 Internal Server Error:未捕获的异常。我要求团队,所有500必须打印完整堆栈,并且触发告警。
- 502 Bad Gateway:上游服务挂了。
- 503 Service Unavailable:服务过载,主动拒绝请求。配合
Retry-After头使用。
避坑指南:我曾经接手过一个老项目,所有错误都返回200,body里code从1001到9999。前端每次对接新接口,都得翻一遍错误码文档。后来我强制推行HTTP语义化,4xx和5xx各归各位,前端只用判断状态码范围,业务错误再看body里的error_code。联调效率直接翻倍。
3.3 错误响应结构:让客户端少写if-else
状态码只解决了「传输层对错」,业务层的错误信息需要一套统一的结构。我见过最糟糕的设计:成功返回{ "data": {...} },失败返回{ "message": "xxx" },字段名都不一样。客户端得先猜这次是成功还是失败,再决定解析哪个字段。
我推荐的结构长这样:
{
"success": true,
"data": {
"id": "12345",
"name": "商品A"
},
"error": null
}
{
"success": false,
"data": null,
"error": {
"code": "INSUFFICIENT_STOCK",
"message": "库存不足,当前可用库存: 5",
"details": {
"sku": "SKU001",
"requested": 10,
"available": 5
}
},
"request_id": "req-20240315-abc123"
}
设计要点:
success:布尔值,客户端第一眼就能判断成败。error.code:业务错误码,用英文大写+下划线,方便国际化。error.message:人类可读的描述,用于日志和调试。error.details:可选,携带具体字段的校验失败信息。request_id:每次请求的唯一标识,排查问题时靠它串联日志。
3.4 知识结构总览
下面这张图,把请求方法、状态码、错误结构串在了一起。你可以把它当作接口建模的「检查清单」。
3.5 实战建议
最后,分享几个我在项目中沉淀下来的习惯:
- 统一响应拦截器:在网关层或服务端框架层,统一包装响应格式。这样每个业务接口只关注自己的
data,不用重复写success和error。 - 错误码枚举化:把
error.code定义成枚举,服务端和客户端各维护一份。我习惯在项目根目录放一个error-codes.md,每次新增错误码都要过评审。 - 请求ID必传:所有入口请求生成唯一
request_id,透传到所有下游服务。排查问题时,一个ID就能串起全链路日志。 - 不要暴露内部细节:5xx的错误信息不要包含堆栈、SQL语句、文件路径。我曾经见过一个接口返回了完整的MySQL错误信息,攻击者直接拿到了表结构——从那以后,我要求所有5xx的
message只能写「内部服务错误」,详细日志走内部通道。