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 知识结构总览

下面这张图,把请求方法、状态码、错误结构串在了一起。你可以把它当作接口建模的「检查清单」。

请求与响应建模知识结构 请求方法 GET POST PUT DELETE PATCH HTTP 状态码 2xx 成功 4xx 客户端 5xx 服务端 错误响应结构 success error.code error.message request_id 设计原则 1. 请求方法语义化:GET查、POST建、PUT替、DELETE删、PATCH改 2. 状态码分层:2xx成功、4xx客户端错、5xx服务端错,不要混用 3. 错误结构统一:success + data + error 三字段,客户端一行代码判断成败 实战案例:创建订单接口 请求:POST /api/orders Body: { "sku": "A001", "quantity": 2 } 成功:201 Created Body: { "success": true, "data": { "order_id": "ORD123" }, "error": null } 失败:422 Unprocessable Entity Body: { "success": false, "data": null, "error": { "code": "INSUFFICIENT_STOCK", "message": "库存不足" } }

3.5 实战建议

最后,分享几个我在项目中沉淀下来的习惯:

  • 统一响应拦截器:在网关层或服务端框架层,统一包装响应格式。这样每个业务接口只关注自己的data,不用重复写success和error。
  • 错误码枚举化:把error.code定义成枚举,服务端和客户端各维护一份。我习惯在项目根目录放一个error-codes.md,每次新增错误码都要过评审。
  • 请求ID必传:所有入口请求生成唯一request_id,透传到所有下游服务。排查问题时,一个ID就能串起全链路日志。
  • 不要暴露内部细节:5xx的错误信息不要包含堆栈、SQL语句、文件路径。我曾经见过一个接口返回了完整的MySQL错误信息,攻击者直接拿到了表结构——从那以后,我要求所有5xx的message只能写「内部服务错误」,详细日志走内部通道。