RESTful API 请求和响应
请求头与响应头的分工、状态码的正确用法、统一响应体结构。
Key Points请求头描述「我想要什么」,响应头描述「你拿到了什么」,状态码描述「结果如何」;业务错误用 4xx,不要一律 200。
请求头
Text
POST /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
Accept: application/json
Idempotency-Key: 3f2a9c10-8b7e-4a1d-9c2f-1e5b7d0a6c33
{"name": "Alice", "email": "alice@example.com"}Authorization— 身份凭证,别放 URL 里(会进日志)Content-Type— 我发的是什么格式Accept— 我想收什么格式If-None-Match— 缓存校验,配合 ETag 使用
状态码
| Code | When |
|---|---|
| 200 OK | Read or update succeeded with a body |
| 201 Created | A resource was created; include Location |
| 204 No Content | Succeeded, nothing to return (DELETE/PUT) |
| 400 Bad Request | Malformed syntax or missing field |
| 401 Unauthorized | Missing or invalid credentials |
| 403 Forbidden | Authenticated but not allowed |
| 404 Not Found | No such resource (or hidden from you) |
| 409 Conflict | State conflict, e.g. duplicate email |
| 422 Unprocessable Entity | Syntax fine, semantics invalid |
| 429 Too Many Requests | Rate limited; send Retry-After |
| 500 / 503 | Server fault / temporarily unavailable |
The classic mistake is returning 200 for everything and putting the error in the body: monitoring, gateways, caches and SDKs all break. The status code is for machines, the error body for humans — provide both.
统一响应体
Return the object itself for a single resource — no {code, data} wrapper. Collections return an object carrying pagination. Error bodies keep a fixed shape so clients can handle them uniformly.
JSON
// 单个资源
{"id": 42, "name": "Alice", "email": "alice@example.com"}
// 列表
{"items": [{"id": 42, "name": "Alice"}], "total": 137, "page": 1, "size": 20}
// 错误
{"error": {"code": "EMAIL_TAKEN", "message": "email already registered",
"details": [{"field": "email", "issue": "duplicate"}]}}内容协商
Text
GET /users/42
Accept: application/json → 200, Content-Type: application/json
Accept: text/csv → 200, Content-Type: text/csv
Accept: application/xml → 406 Not Acceptable(不支持)几个完整例子
Text
# 创建成功
HTTP/1.1 201 Created
Location: /api/users/42
{"id": 42, "name": "Alice"}
# 资源不存在
HTTP/1.1 404 Not Found
{"error": {"code": "NOT_FOUND", "message": "user 42 does not exist"}}
# 参数不合法
HTTP/1.1 422 Unprocessable Entity
{"error": {"code": "VALIDATION_FAILED", "message": "invalid input",
"details": [{"field": "age", "issue": "must be >= 0"}]}}请求头
Text
POST /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
Accept: application/json
Idempotency-Key: 3f2a9c10-8b7e-4a1d-9c2f-1e5b7d0a6c33
{"name": "Alice", "email": "alice@example.com"}Authorization—— 身份凭证,别放 URL 里(会进日志)Content-Type—— 我发的是什么格式Accept—— 我想收什么格式If-None-Match—— 缓存校验,配合 ETag 使用
状态码
| 状态码 | 何时用 |
|---|---|
| 200 OK | 读取或更新成功,带响应体 |
| 201 Created | 资源已创建,带 Location |
| 204 No Content | 成功但无内容(DELETE/PUT) |
| 400 Bad Request | 语法错误或缺少字段 |
| 401 Unauthorized | 缺少或无效凭证 |
| 403 Forbidden | 已认证但无权限 |
| 404 Not Found | 资源不存在(或对你不可见) |
| 409 Conflict | 状态冲突,如邮箱重复 |
| 422 Unprocessable Entity | 语法正确但语义不合法 |
| 429 Too Many Requests | 被限流,带 Retry-After |
| 500 / 503 | 服务端故障 / 暂时不可用 |
最常见的错误是「一切 200,错误写在 body 里」。这样监控、网关、缓存、客户端 SDK 全部失效。状态码是给机器看的,body 里的错误详情是给人看的,两者都要有。
统一响应体
单个资源直接返回对象本身,不要套一层 {code, data};列表返回一个带分页信息的对象。错误体固定三个字段,客户端才好统一处理。
JSON
// 单个资源
{"id": 42, "name": "Alice", "email": "alice@example.com"}
// 列表
{"items": [{"id": 42, "name": "Alice"}], "total": 137, "page": 1, "size": 20}
// 错误
{"error": {"code": "EMAIL_TAKEN", "message": "email already registered",
"details": [{"field": "email", "issue": "duplicate"}]}}内容协商
Text
GET /users/42
Accept: application/json → 200, Content-Type: application/json
Accept: text/csv → 200, Content-Type: text/csv
Accept: application/xml → 406 Not Acceptable(不支持)几个完整例子
Text
# 创建成功
HTTP/1.1 201 Created
Location: /api/users/42
{"id": 42, "name": "Alice"}
# 资源不存在
HTTP/1.1 404 Not Found
{"error": {"code": "NOT_FOUND", "message": "user 42 does not exist"}}
# 参数不合法
HTTP/1.1 422 Unprocessable Entity
{"error": {"code": "VALIDATION_FAILED", "message": "invalid input",
"details": [{"field": "age", "issue": "must be >= 0"}]}}