成功只有一个结果,失败却有多种原因
以前我对 API 响应的理解是:无论成功还是失败,都应该用统一的模型包装,例如 { code, message, data },通过 code 判断成功与否,data 承载结果。以前公司的设计也是如此——所有接口都返回带包装的格式,成功和失败共用一套信封。
经过一轮思考和对比主流实践后,我的结论是:成功和失败的信息结构应当不同。成功时,依靠 HTTP 状态码即可表达「请求成功」,Body 直接返回资源;失败时,则需要用结构化的 code 和 message 同时告诉程序(为何失败、如何分支)和人(如何展示、如何解释)。信封式的统一包装并非必须,甚至会带来冗余。
下文展开这种设计背后的逻辑与取舍。
一、两种设计路线
API 响应模型本质要回答三个问题:客户端如何知道请求成功还是失败?成功时数据在哪?失败时如何得知原因?
由此衍生出两条路线:
| 路线 | 思路 | 成功 | 失败 |
|---|---|---|---|
| 用 HTTP 表达语义 | 状态码负责成功/失败,Body 负责业务内容 | 200 + 直接返回资源 | 4xx/5xx + 错误描述 |
| 用 Body 表达语义 | 不依赖状态码 | { success: true, data } | { success: false, error } |
主流实践(Stripe、GitHub、微信、支付宝)多采用前者:让 HTTP 状态码承担「成功/失败」的语义,Body 专注承载业务数据或错误详情。
二、成功响应:为什么直接返回资源?
设计理念:成功时,客户端要的就是资源本身。
- 资源已包含所需信息,无需再包一层
{ data: resource } - 解析更直接:
const user = response而不是const user = response.data - 减少冗余:200 已表示成功,
success: true显得多余
因此常见做法是:
GET /users/123 → 200
{ "id": "123", "display_name": "张三", ... }而不是:
{ "success": true, "data": { "id": "123", ... } }三、失败响应:为什么需要 code 和 message?
设计理念:失败时,信息是不对称的——原因多样,需要结构化表达。
HTTP 状态码能表达的
| 状态码 | 含义 |
|---|---|
| 400 | 请求有问题 |
| 401 | 未认证 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 422 | 校验失败 |
| 429 | 请求过多 |
| 500 | 服务端错误 |
状态码只告诉你错误大类,不告诉你具体是哪一种。同为 400,可能是手机号格式错误、验证码错误、验证码过期——客户端无法区分。
code 和 message 各司其职
| 字段 | 受众 | 作用 |
|---|---|---|
code | 程序 | 机器可读,用于分支逻辑、重试策略、监控统计 |
message | 用户 | 人类可读,用于 Toast/Alert 直接展示 |
示例:
if error.code == "INVALID_CODE" {
showAlert("验证码错误,请重新输入")
} else if error.code == "CODE_EXPIRED" {
showAlert("验证码已过期,请重新获取")
}小结:成功与失败的不对称性
- 成功:结果单一,靠 HTTP 状态码(200)表达成功,Body 直接返回资源即可。
- 失败:原因多样,需要同时告诉人和机器——
code给程序做分支与监控,message给人展示,必要时用details给出校验明细。
这种不对称性,正是成功和失败设计哲学不同的根源。
四、主流实践一览
| 服务 | 成功 | 错误 |
|---|---|---|
| Stripe | 直接返回对象 | { "error": { "code": "...", "message": "..." } } |
| GitHub | 直接返回对象/数组 | { "message": "...", "errors": [...] } |
| 微信开放平台 | 直接返回业务字段 | { "errcode": 40001, "errmsg": "..." } |
| 支付宝 | 直接返回业务字段 | { "code": "10000", "msg": "...", "sub_code": "..." } |
共同点:成功时直接返回资源,失败时用简单 JSON 表达 code/message,不套多层 envelope。
五、推荐的错误结构
{
"error": {
"code": "INVALID_CODE",
"message": "验证码错误或已过期",
"details": [
{ "field": "code", "message": "必须是 6 位数字" }
]
}
}code:业务错误码,用于程序判断。message:用户可见说明。details:可选,校验失败时逐字段说明。
六、设计原则小结
- HTTP 能表达的交给 HTTP:状态码表示 success/fail,Body 表示业务内容。
- 成功时 Body 就是你要的:不必再包一层
data。 - 失败时需要结构化:
code+message,必要时加details。 - 简单优于复杂:够用即可,不追求 RFC 7807 等完整规范。
- 保持一致性:成功和失败都用统一格式,减少客户端分支逻辑。


