外观
RESTful API 与后端接口设计学习文档
更新日期:2026-07-02
适用范围:Web 后端、前后端分离项目、BFF、移动端 API、企业内部管理系统、AI/Agent 后端工具接口。
主要依据:IETF RFC 9110、MDN HTTP 文档、Roy Fielding REST 论文、Google AIP、Microsoft REST API Guidelines、OpenAPI Specification、RFC 9457、OWASP API Security Top 10。
1. REST 是什么
REST 是 Representational State Transfer,中文常译为“表述性状态转移”。它不是某个框架,也不是某个协议,而是一种用于设计分布式网络应用的架构风格。
在日常后端开发里,我们说的 REST API 通常是指:
text
用 URL 表示资源
用 HTTP 方法表示动作
用 HTTP 状态码表示结果
用 JSON 表示数据
每次请求都自包含,不依赖服务端保存会话状态例如:
http
GET /api/v1/users/1001
POST /api/v1/users
PATCH /api/v1/users/1001
DELETE /api/v1/users/1001这组接口表达的是:
text
查询用户 1001
创建用户
修改用户 1001 的部分字段
删除用户 1001REST 的关键不是“URL 长得像不像”,而是接口是否围绕资源建模,是否遵守 HTTP 语义,是否能让前端、后端、网关、缓存、监控和文档工具都清楚理解这个接口在做什么。
2. REST、HTTP、JSON、RPC 的区别
很多人把 REST、HTTP、JSON 混在一起,这是入门时最大的误区。
| 概念 | 是什么 | 说明 |
|---|---|---|
| REST | 架构风格 | 关注资源、表述、无状态、统一接口、缓存、分层系统 |
| HTTP | 应用层协议 | 提供方法、状态码、请求头、响应头、缓存、认证、条件请求等语义 |
| JSON | 数据格式 | 常用于请求体和响应体,也可以用 XML、HTML、CSV、NDJSON |
| RPC | 远程过程调用风格 | 更关注“调用一个动作”,例如 /createUser、/cancelOrder |
REST 更偏资源:
http
POST /api/v1/usersRPC 更偏动作:
http
POST /api/v1/createUser真实项目里不需要极端。CRUD 和资源查询优先使用 REST 风格;当业务动作无法自然映射成标准 CRUD 时,可以用少量 command-style endpoint。
例如取消订单:
http
POST /api/v1/orders/{orderId}/cancel这不是纯粹的 CRUD,但在业务上清晰、稳定、可维护。
3. REST 的核心约束
REST 原始架构风格强调几类约束:
| 约束 | 含义 | 对后端接口设计的影响 |
|---|---|---|
| Client-Server | 客户端和服务端职责分离 | 前端负责展示和交互,后端负责资源和业务规则 |
| Stateless | 请求无状态 | 每个请求都带上认证、参数和上下文,不依赖服务端临时会话 |
| Cacheable | 响应可声明是否可缓存 | 合理使用 Cache-Control、ETag、Last-Modified |
| Uniform Interface | 统一接口 | URL、HTTP 方法、状态码、媒体类型语义一致 |
| Layered System | 分层系统 | 客户端不需要知道请求经过网关、代理、CDN 还是 BFF |
| Code on Demand | 可选 | 服务端可下发可执行逻辑,Web API 通常不重点依赖它 |
对于大多数业务系统,最重要的是:
text
资源建模清楚
HTTP 方法语义正确
接口无状态
错误格式统一
支持缓存、权限、幂等、追踪和文档化3.1 被忽略的约束:HATEOAS
「统一接口」里最容易被忽略的一条是 HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用状态的引擎)。它的意思是:服务端不该只返回资源数据,还应该返回下一步可以做什么——客户端依据响应里的链接来推进状态,而不是把 URL 拼写逻辑硬编码在客户端里。
对比:
json
// 无 HATEOAS:客户端必须自己知道怎么取消订单
{ "id": "order_1001", "status": "paid" }json
// 有 HATEOAS:服务端告诉客户端当前可用的动作
{
"id": "order_1001",
"status": "paid",
"_links": {
"self": { "href": "/api/v1/orders/order_1001" },
"cancel": { "href": "/api/v1/orders/order_1001/cancel", "method": "POST" },
"refund": { "href": "/api/v1/orders/order_1001/refund", "method": "POST" }
}
}关键点:
- 链接是否出现本身携带业务状态:
cancel链接只在订单可取消时出现,已发货的订单自然没有它——客户端不用再写一套「什么状态下能点取消」的判断逻辑。 - 现实项目中,移动端、企业内部系统、前后端紧耦合的团队很少做满 HATEOAS,因为多一次网络往返、前端也要多一层解析,收益在团队小、接口少时不明显。
- 但它对「接口长期演进」价值很大:改 URL、拆分服务时,客户端只要跟着
_links走,不用改自己的硬编码。
3.2 Richardson 成熟度模型
REST 的实现程度可以用 Richardson Maturity Model(Richardson 成熟度模型)分 4 级评估,它常被用来衡量一个「RESTful」接口到底有多 REST:
| 级别 | 名称 | 特征 | 示例 |
|---|---|---|---|
| Level 0 | The Swamp of POX | 只有一个 endpoint,全用 POST 传动作 | POST /api/do |
| Level 1 | Resources | 每个资源有独立 URL | GET /users/1、GET /orders/2 |
| Level 2 | HTTP Verbs | 用 HTTP 方法表达动作,配合状态码 | POST /users、DELETE /users/1 |
| Level 3 | Hypermedia Controls | 在 Level 2 基础上加 HATEOAS 链接 | 上面带 _links 的例子 |
⚠️ 注意:Level 3 不等于「更好」。绝大多数生产系统做到 Level 2 就足够清晰可维护,HATEOAS 是可选增强而非必须。Level 0 才是真正要避免的——所有操作挤在一个接口里用业务参数区分,等于放弃了 HTTP 的全部语义。
4. 后端接口设计的总流程
设计一个后端接口,不要一上来写 Controller。先按这个顺序思考:
text
业务场景
-> 识别资源
-> 识别资源关系
-> 设计 URL
-> 选择 HTTP 方法
-> 设计请求参数
-> 设计响应结构
-> 设计状态码和错误码
-> 补充权限、幂等、缓存、并发控制
-> 写 OpenAPI 文档
-> 写测试和联调用例以订单系统为例:
text
资源:用户、订单、订单明细、商品、支付单、退款单、发票
关系:用户有订单,订单有明细,订单关联支付单,支付单可产生退款单接口可能是:
http
GET /api/v1/orders
GET /api/v1/orders/{orderId}
POST /api/v1/orders
POST /api/v1/orders/{orderId}/cancel
GET /api/v1/orders/{orderId}/items
GET /api/v1/orders/{orderId}/payments
POST /api/v1/orders/{orderId}/refunds5. URL 应该怎么设计
5.1 URL 用名词,不用动词
推荐:
http
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/{userId}
PATCH /api/v1/users/{userId}
DELETE /api/v1/users/{userId}不推荐:
http
GET /api/v1/getUser
POST /api/v1/createUser
POST /api/v1/updateUser
POST /api/v1/deleteUser原因是:HTTP 方法已经表达动作,URL 应该表达资源。
5.2 资源名使用复数
推荐统一用复数集合名:
http
/users
/orders
/products
/roles
/permissions不要混用:
http
/user
/users
/orderList
/productInfo5.3 层级不要太深
可以表达强归属关系:
http
GET /api/v1/users/{userId}/orders
GET /api/v1/orders/{orderId}/items不要无限嵌套:
http
GET /api/v1/companies/{companyId}/departments/{departmentId}/users/{userId}/orders/{orderId}/items/{itemId}层级太深时,通常说明你应该把某个资源独立出来:
http
GET /api/v1/order-items/{itemId}
GET /api/v1/orders/{orderId}/items5.4 不要让 URL 暴露数据库结构
API 资源模型不等于数据库表结构。数据库里可能有 20 张表,但 API 不应该机械映射成 20 个裸表接口。
不推荐:
http
GET /api/v1/t_user
GET /api/v1/t_order_header
GET /api/v1/t_order_detail推荐:
http
GET /api/v1/users
GET /api/v1/orders
GET /api/v1/orders/{orderId}API 是产品契约,不是数据库暴露层。
6. HTTP 方法怎么选
| 方法 | 典型用途 | 是否安全 | 是否幂等 | 示例 |
|---|---|---|---|---|
GET | 查询资源 | 是 | 是 | GET /users/1001 |
HEAD | 只取响应头 | 是 | 是 | HEAD /files/abc |
POST | 创建资源、提交命令 | 否 | 通常否 | POST /orders |
PUT | 整体替换资源 | 否 | 是 | PUT /users/1001/profile |
PATCH | 局部修改资源 | 否 | 通常否 | PATCH /users/1001 |
DELETE | 删除资源 | 否 | 是 | DELETE /users/1001 |
OPTIONS | 查询通信选项 | 是 | 是 | CORS 预检 |
6.1 GET
用于查询,不应该改变服务端业务状态:
http
GET /api/v1/users/1001
GET /api/v1/orders?status=paid不要这样设计:
http
GET /api/v1/orders/1001/cancel因为取消订单会改变状态,不应该用 GET。
6.2 POST
用于创建资源或提交业务命令:
http
POST /api/v1/orders
POST /api/v1/orders/{orderId}/cancel
POST /api/v1/import-jobsPOST 默认不是幂等的。网络重试可能造成重复创建,所以创建订单、支付、提交表单时要考虑幂等键。
6.3 PUT
用于整体替换。客户端发送完整资源表示:
http
PUT /api/v1/users/1001/profile
Content-Type: application/json
{
"displayName": "Alice",
"avatarUrl": "https://example.com/a.png",
"bio": "Frontend developer"
}PUT 的语义接近“把这个资源替换成请求体这样”。
6.4 PATCH
用于局部更新:
http
PATCH /api/v1/users/1001
Content-Type: application/json
{
"displayName": "Alice"
}后台管理系统里,修改用户状态、昵称、角色等局部字段,一般用 PATCH 更合适。
6.5 DELETE
用于删除资源:
http
DELETE /api/v1/users/1001删除成功常返回:
http
204 No Content如果业务是软删除,也仍然可以用 DELETE。接口语义是“这个资源对调用者不可再作为正常资源使用”,不要求物理删除数据库行。
7. 状态码怎么用
状态码是 HTTP 层面的结果,不要所有接口都返回 200 再靠业务字段判断成功失败。
7.1 成功状态码
| 状态码 | 场景 |
|---|---|
200 OK | 查询成功、更新后返回资源 |
201 Created | 创建成功 |
202 Accepted | 请求已接收,但异步处理未完成 |
204 No Content | 成功但无响应体,常用于删除 |
创建用户示例:
http
POST /api/v1/users
201 Created
Location: /api/v1/users/1001异步导入示例:
http
POST /api/v1/import-jobs
202 Accepted
Location: /api/v1/import-jobs/job_1237.2 客户端错误状态码
| 状态码 | 场景 |
|---|---|
400 Bad Request | 请求语法或基础参数错误 |
401 Unauthorized | 未认证或认证失效 |
403 Forbidden | 已认证但无权限 |
404 Not Found | 资源不存在 |
409 Conflict | 当前资源状态冲突 |
412 Precondition Failed | 条件请求失败,例如 ETag 不匹配 |
415 Unsupported Media Type | 请求体格式不支持 |
422 Unprocessable Content | JSON 格式正确,但业务语义校验失败 |
429 Too Many Requests | 触发限流 |
常见选择:
text
字段缺失、JSON 格式错 -> 400
没登录 -> 401
没权限 -> 403
查不到资源 -> 404
重复提交、状态不允许 -> 409
业务字段校验失败 -> 4227.3 服务端错误状态码
| 状态码 | 场景 |
|---|---|
500 Internal Server Error | 未预期服务端错误 |
502 Bad Gateway | 网关收到上游错误 |
503 Service Unavailable | 服务不可用、维护、过载 |
504 Gateway Timeout | 上游超时 |
服务端错误不要把堆栈、SQL、内部类名直接返回给前端。
7.4 容易搞混的几组状态码(内部机制)
401 vs 403:这是被误解最多的一组。401 的官方语义是「未通过认证(unauthorized)」——准确说是不知道你是谁;403 是「已认证但被拒绝(forbidden)」——知道你是谁,但不许你做。之所以名字这么绕,是因为 401 的正式名称 Unauthorized 其实更贴近「unauthenticated」的意思。判断口诀:登录问题 → 401,权限问题 → 403。
409 vs 422:都是「请求在业务上不成立」,但触发点不同。422 是请求体本身语义校验不过(字段格式对但业务规则不允许,如「年龄不能为负」);409 是请求与服务器当前资源状态冲突(如邮箱已存在、订单已支付不能再取消)。409 通常意味着「重试也没用,得先改变服务器状态」,422 意味着「改了请求体也许能成」。
204 vs 200(空响应体):删除成功、无内容可返回时用 204 No Content,它明确表示响应体必须为空,客户端看到 204 就不该尝试解析 body。用 200 配一个空 JSON {} 也能工作,但语义不如 204 干净,且部分代理/网关对 204 有专门优化。
301 vs 302 vs 307 vs 308:重定向四兄弟。301/308 是永久重定向(308 保方法、301 可能把 POST 变 GET),302/307 是临时重定向(307 保方法、302 可能变 GET)。API 里如果要重定向,绝大多数场景用 307(临时、保留方法)最安全。
8. 请求参数怎么设计
后端接口参数通常分四类:
| 位置 | 用途 | 示例 |
|---|---|---|
| Path Parameter | 定位资源 | /users/{userId} |
| Query Parameter | 筛选、排序、分页、搜索 | ?page=1&pageSize=20 |
| Header | 认证、追踪、条件请求、幂等键 | Authorization、X-Request-Id |
| Body | 创建或修改资源的数据 | JSON 请求体 |
8.1 Path 参数
只放资源标识:
http
GET /api/v1/users/{userId}
GET /api/v1/orders/{orderId}不要把复杂筛选条件放进 path:
http
GET /api/v1/orders/status/paid/date/2026-07-02应该用 query:
http
GET /api/v1/orders?status=paid&createdDate=2026-07-028.2 Query 参数
查询列表时常用:
http
GET /api/v1/orders?page=1&pageSize=20&status=paid&sort=createdAt,desc推荐约定:
text
page:页码,从 1 开始
pageSize:每页数量
sort:排序字段和方向,例如 createdAt,desc
q:全文搜索关键字
status:业务状态筛选
createdFrom / createdTo:时间范围8.3 Header
常用 header:
http
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json
X-Request-Id: req_abc123
Idempotency-Key: idem_abc123
If-Match: "etag-value"8.4 Body
创建用户:
http
POST /api/v1/users
Content-Type: application/json
{
"username": "alice",
"email": "alice@example.com",
"displayName": "Alice"
}局部修改:
http
PATCH /api/v1/users/1001
Content-Type: application/json
{
"displayName": "Alice Zhang"
}不要让前端传服务端生成字段:
json
{
"id": "1001",
"createdAt": "2026-07-02T10:00:00Z",
"updatedAt": "2026-07-02T10:00:00Z"
}这些字段应该由服务端生成。
9. 响应结构怎么设计
9.1 详情接口
http
GET /api/v1/users/1001json
{
"id": "1001",
"username": "alice",
"email": "alice@example.com",
"displayName": "Alice",
"status": "active",
"createdAt": "2026-07-02T10:00:00Z",
"updatedAt": "2026-07-02T10:30:00Z"
}详情接口可以直接返回资源对象,不一定非要包一层 data。如果团队统一要求包裹,也可以这样:
json
{
"data": {
"id": "1001",
"username": "alice"
}
}关键是全项目一致。
9.2 列表接口
推荐:
json
{
"data": [
{
"id": "order_1001",
"status": "paid",
"amount": 29900,
"currency": "CNY"
}
],
"meta": {
"page": 1,
"pageSize": 20,
"total": 156
}
}如果数据量很大,total 查询成本高,可以使用 cursor 分页:
json
{
"data": [
{
"id": "order_1001",
"createdAt": "2026-07-02T10:00:00Z"
}
],
"pageInfo": {
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA3LTAyVDEwOjAwOjAwWiJ9",
"hasNextPage": true
}
}9.3 创建接口
http
POST /api/v1/orders推荐返回创建后的资源:
http
201 Created
Location: /api/v1/orders/order_1001json
{
"id": "order_1001",
"status": "pending_payment",
"amount": 29900,
"createdAt": "2026-07-02T10:00:00Z"
}9.4 删除接口
http
DELETE /api/v1/users/1001推荐:
http
204 No Content不要返回:
json
{
"success": true
}如果团队统一包装响应,返回 200 加结果也能工作,但从 HTTP 语义上,204 更干净。
10. 错误响应怎么设计
推荐参考 RFC 9457 的 Problem Details 思路:错误响应应该能表达错误类型、标题、状态码、详细信息和本次问题实例。
通用格式:
json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "Request body contains invalid fields.",
"instance": "/api/v1/users",
"errors": [
{
"field": "email",
"code": "INVALID_EMAIL",
"message": "email must be a valid email address"
}
],
"traceId": "req_abc123"
}如果团队觉得 RFC 9457 太正式,也可以用内部统一格式:
json
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"details": [],
"traceId": "req_abc123"
}
}关键要求:
- 错误格式统一。
- 有稳定的机器可读
code。 - 有人类可读
message。 - 参数错误能定位字段。
- 有
traceId方便排查。 - 不能泄露 SQL、堆栈、密钥、内部服务地址。
11. 分页、筛选、排序和搜索
11.1 页码分页
适合后台管理列表:
http
GET /api/v1/users?page=1&pageSize=20响应:
json
{
"data": [],
"meta": {
"page": 1,
"pageSize": 20,
"total": 100
}
}优点是前端好做分页器;缺点是深页查询和大表 count 可能慢。
11.2 Cursor 分页
适合时间线、消息流、大数据列表:
http
GET /api/v1/messages?limit=20&cursor=xxx响应:
json
{
"data": [],
"pageInfo": {
"nextCursor": "yyy",
"hasNextPage": true
}
}优点是性能更稳定;缺点是不适合直接跳到第 N 页。
11.3 筛选
http
GET /api/v1/orders?status=paid&createdFrom=2026-07-01&createdTo=2026-07-31复杂筛选可以用约定:
http
GET /api/v1/orders?filter[status]=paid&filter[channel]=web或者在复杂报表场景使用 POST 查询:
http
POST /api/v1/order-searchjson
{
"status": ["paid", "shipped"],
"createdAt": {
"from": "2026-07-01",
"to": "2026-07-31"
},
"sort": [
{
"field": "createdAt",
"direction": "desc"
}
]
}注意:使用 POST 做复杂查询时,语义更接近“创建一次查询任务/提交复杂查询条件”,要在团队规范里明确。
11.4 排序
简单写法:
http
GET /api/v1/orders?sort=createdAt,desc多字段:
http
GET /api/v1/orders?sort=createdAt,desc&sort=amount,asc后端必须限制可排序字段,避免 SQL 注入或慢查询。
12. 资源关系怎么设计
12.1 父子资源
用户订单:
http
GET /api/v1/users/{userId}/orders订单明细:
http
GET /api/v1/orders/{orderId}/items12.2 独立资源
如果子资源有独立生命周期,可以给它独立 endpoint:
http
GET /api/v1/order-items/{itemId}
PATCH /api/v1/order-items/{itemId}12.3 关联关系
角色和权限:
http
GET /api/v1/roles/{roleId}/permissions
PUT /api/v1/roles/{roleId}/permissions
POST /api/v1/roles/{roleId}/permissions/{permissionId}
DELETE /api/v1/roles/{roleId}/permissions/{permissionId}如果是整体替换某角色权限集合,用 PUT:
http
PUT /api/v1/roles/admin/permissionsjson
{
"permissionIds": ["user:read", "user:write", "order:read"]
}如果是添加一个权限,可以用 POST:
http
POST /api/v1/roles/admin/permissions/user:read13. 业务动作怎么设计
不是所有业务都能自然映射成 CRUD。遇到动作时有三种设计方式。
13.1 作为资源状态更新
修改订单状态:
http
PATCH /api/v1/orders/{orderId}json
{
"status": "cancelled"
}适合简单状态修改。
13.2 作为子资源创建
退款是一种独立业务记录:
http
POST /api/v1/orders/{orderId}/refundsjson
{
"amount": 10000,
"reason": "Customer request"
}适合会留下记录、有生命周期的动作。
13.3 作为命令接口
发布文章、取消订单、审批通过:
http
POST /api/v1/articles/{articleId}/publish
POST /api/v1/orders/{orderId}/cancel
POST /api/v1/approvals/{approvalId}/approve适合业务语义明确、难以用标准 CRUD 表达的动作。
注意:命令接口不要滥用。能用资源建模表达时,优先资源建模。
14. 幂等性和防重复提交
幂等的意思是:同一个请求执行一次和执行多次,服务端目标状态相同。
HTTP 语义中,GET、HEAD、PUT、DELETE 通常是幂等的,POST 通常不是。
14.1 创建订单为什么要幂等
用户点击支付按钮,网络超时,前端重试。如果后端没有幂等控制,可能创建两笔订单或扣款两次。
推荐:
http
POST /api/v1/orders
Idempotency-Key: idem_20260702_abc123json
{
"productId": "prod_1001",
"quantity": 1
}后端处理:
text
同一用户 + 同一 Idempotency-Key + 同一接口
-> 第一次执行并保存结果
-> 后续重复请求直接返回第一次结果适用场景:
- 创建订单。
- 支付。
- 提交审批。
- 导入任务。
- 发送通知。
- 任何可能因重试造成重复副作用的 POST。
14.2 前端防抖不是幂等
按钮 loading、防重复点击只是用户体验层。真正的幂等必须在服务端实现。
14.3 Idempotency-Key 的内部机制
Idempotency-Key 不是简单「查一下有没有重复」就完了,服务端要做三件事:
唯一性范围:幂等键必须限定在「同一用户 + 同一接口」范围内才有意义。两个不同用户各自传
idem_abc123不该互相冲突;用户 A 在POST /orders和POST /refunds传同一个键,也应该被视为两个不同操作。实现上通常用(userId, endpoint, key)作为数据库唯一索引。结果存储:第一次请求成功后,把完整的响应(状态码 + 响应体)连同键一起存下来,设置一个过期时间(常见 24 小时)。后续重复请求直接返回缓存的响应,而不是重新执行。⚠️ 只存「已处理过」这个标志而不存响应体,会导致重试时返回不了第一次的结果,破坏幂等语义。
并发冲突:两个携带相同键的请求同时到达时,唯一索引会拒绝其中一个。被拒绝的请求不该直接返回 500,而应该等待并返回第一个请求的结果(返回 409 让客户端重新查询,或服务端阻塞等待后返回已有结果)。
text
收到请求
-> 查 (user, endpoint, key) 是否已存在
-> 不存在:执行 + 保存结果(唯一索引兜底并发)
-> 存在:直接返回已存的结果,不重复执行⚠️ 常见坑:用 Idempotency-Key 却只在内存里存去重标志(重启就丢),或没设过期时间(表无限膨胀)。这两点都会让幂等控制形同虚设。
15. 并发控制与 ETag
并发修改常见问题:
text
用户 A 打开订单
用户 B 修改订单
用户 A 用旧数据保存
用户 B 的修改被覆盖可以用 ETag + If-Match 做乐观并发控制。
查询:
http
GET /api/v1/orders/order_1001响应:
http
ETag: "v3"更新:
http
PATCH /api/v1/orders/order_1001
If-Match: "v3"如果服务端当前版本已经不是 v3:
http
412 Precondition Failed适合:
- 订单状态修改。
- 文档协作。
- 配置管理。
- 后台管理编辑表单。
简单系统也可以用 version 字段:
json
{
"displayName": "Alice",
"version": 3
}但 HTTP 层面的 ETag 更标准。
15.1 ETag 的强弱校验(内部机制)
ETag 有两种强度,语义完全不同:
- 强校验(Strong Validation):
ETag: "v3"—— 表示资源逐字节相同。乐观锁用强 ETag,If-Match: "v3"只有在资源当前版本完全等于v3时才通过。 - 弱校验(Weak Validation):
ETag: W/"v3"(带W/前缀)—— 表示资源语义等价但字节可能不同,例如同一个文档被不同服务器生成了格式略有差异的版本。弱 ETag 只能用于If-None-Match(协商缓存),不能用于If-Match的乐观锁,因为「语义等价」不等于「没被别人改过」。
text
乐观锁(防覆盖) -> 强 ETag + If-Match
协商缓存(省流量) -> 强或弱 ETag + If-None-Match⚠️ 坑:如果并发控制用 ETag,一定不能生成弱 ETag,否则两个用户基于不同字节版本的数据都能通过 If-Match 校验,覆盖照样发生。
16. 缓存设计
REST 和 HTTP 天然支持缓存。接口是否能缓存,应该由响应头明确表达。
16.1 不缓存
用户私有数据:
http
Cache-Control: no-store适合:
- 用户资料。
- 订单详情。
- 权限菜单。
- 敏感业务数据。
16.2 短缓存
公共配置:
http
Cache-Control: public, max-age=6016.3 协商缓存
http
ETag: "abc123"
Last-Modified: Thu, 02 Jul 2026 10:00:00 GMT客户端下次请求:
http
If-None-Match: "abc123"没有变化:
http
304 Not Modified适合:
- 字典配置。
- 静态元数据。
- 文档内容。
- 商品类目。
注意:带用户权限的数据不要随意共享缓存。
16.4 no-cache vs no-store vs must-revalidate(最易混的三个指令)
这三个 Cache-Control 指令名字像、语义完全不同,是缓存设计里最常被用错的地方:
| 指令 | 语义 | 通俗理解 |
|---|---|---|
no-store | 完全不缓存,任何环节(浏览器、CDN、代理)都不许存副本 | 「每次都必须到源站要,且不留底」 |
no-cache | 可以缓存,但每次用之前必须先向源站重新验证(发条件请求) | 「存着可以,但用之前问一句源站变没变」 |
must-revalidate | 缓存过期后,必须回源站验证;源站不可达时不得使用过期缓存 | 「过期了就必须重新验证,验证不了就报错」 |
text
私密数据(余额、订单) -> no-store
可缓存但要保证新鲜(配置) -> no-cache
公共资源但过期后不能糊弄(价格) -> max-age=60, must-revalidate⚠️ 最常见错误:把 no-cache 当成「不缓存」来用。实际上它允许缓存,只是强制每次重新验证——想表达「绝不缓存」应该用 no-store。两者写反,会导致敏感数据被 CDN 或浏览器缓存,是安全风险。
17. 认证与授权
17.1 认证
认证回答:你是谁?
常见方式:
http
Authorization: Bearer <access_token>未登录或 token 无效:
http
401 Unauthorized17.2 授权
授权回答:你能不能访问这个资源?
已登录但没权限:
http
403 Forbidden后端必须在每个资源级接口校验权限,不能只依赖前端隐藏按钮。
错误示例:
http
GET /api/v1/orders/order_1001只判断用户已登录,不判断这个订单是否属于当前用户或当前用户是否有组织权限。
正确做法:
text
认证当前用户
-> 查询用户所属租户/组织/角色
-> 判断是否能访问 order_1001
-> 返回资源或 403/404有些系统为了避免泄露资源是否存在,会对无权访问的资源返回 404,而不是 403。这要在团队安全规范里统一。
18. API 安全设计
根据 OWASP API Security Top 10,接口设计至少要关注:
| 风险 | 接口设计中的防护 |
|---|---|
| 对象级越权 | 每次按资源校验权限,不信任前端传来的 userId / tenantId |
| 认证失效 | token 有效期、刷新机制、撤销机制 |
| 属性级越权 | 不允许前端提交敏感字段,例如 role=admin |
| 资源消耗无限制 | 分页上限、限流、上传大小限制、超时 |
| 功能级越权 | 后端校验角色和权限,不只控制菜单 |
| 业务流程滥用 | 验证码、风控、审批、频率限制 |
| SSRF | 外部 URL 访问必须白名单和内网 IP 检查 |
| 安全配置错误 | CORS、错误泄露、默认账号、调试接口 |
| API 资产管理不当 | 旧版本接口、测试接口、未登记接口 |
| 不安全第三方 API | 调外部接口要超时、重试、签名、校验响应 |
接口层基本要求:
- 所有输入都校验。
- 所有写操作都鉴权。
- 资源级权限不能省略。
- 请求体字段白名单。
- 上传文件限制类型、大小和存储位置。
- 错误响应不暴露内部实现。
- 记录审计日志。
- 对关键接口限流。
19. 版本设计
常见版本方式:
http
/api/v1/users优点是简单清楚,适合大多数项目。
也可以用 header:
http
Accept: application/vnd.example.v1+json但前后端项目、企业内部系统、后台管理系统一般用 path version 更直接。
版本原则:
- 小改动不一定升大版本。
- 不兼容变更才需要新版本。
- 不要长期维护太多版本。
- 废弃旧版本要有迁移周期。
- OpenAPI 文档也要按版本维护。
不兼容变更包括:
- 删除字段。
- 改字段类型。
- 改枚举含义。
- 改状态码语义。
- 改分页格式。
- 改认证方式。
兼容变更包括:
- 新增可选字段。
- 新增 endpoint。
- 新增枚举值,但前端必须能容错。
20. 长任务和异步任务接口
导入、导出、批量处理、AI 生成、大文件处理都不适合让 HTTP 请求一直等待。
推荐设计:
http
POST /api/v1/export-jobsjson
{
"type": "orders",
"filters": {
"status": "paid"
}
}响应:
http
202 Accepted
Location: /api/v1/export-jobs/job_1001json
{
"id": "job_1001",
"status": "queued"
}查询任务:
http
GET /api/v1/export-jobs/job_1001json
{
"id": "job_1001",
"status": "succeeded",
"downloadUrl": "https://example.com/files/export.xlsx",
"createdAt": "2026-07-02T10:00:00Z",
"finishedAt": "2026-07-02T10:03:00Z"
}任务状态建议:
text
queued
running
succeeded
failed
cancelled21. 文件上传和下载接口
21.1 小文件上传
http
POST /api/v1/files
Content-Type: multipart/form-data响应:
json
{
"id": "file_1001",
"name": "avatar.png",
"contentType": "image/png",
"size": 123456,
"url": "https://cdn.example.com/files/file_1001"
}21.2 大文件上传
大文件建议分片或直传对象存储:
http
POST /api/v1/upload-sessionsjson
{
"fileName": "video.mp4",
"contentType": "video/mp4",
"size": 104857600
}响应:
json
{
"uploadSessionId": "upl_1001",
"parts": [
{
"partNumber": 1,
"uploadUrl": "https://storage.example.com/..."
}
]
}21.3 下载
如果需要鉴权:
http
GET /api/v1/files/{fileId}/download-url返回短期有效签名 URL:
json
{
"url": "https://storage.example.com/file?signature=xxx",
"expiresAt": "2026-07-02T10:10:00Z"
}22. 批量接口怎么设计
批量操作不要用循环请求硬怼后端。
批量删除:
http
POST /api/v1/users:batchDeletejson
{
"ids": ["1001", "1002", "1003"]
}或者:
http
POST /api/v1/batch-operationsjson
{
"operation": "deleteUsers",
"userIds": ["1001", "1002", "1003"]
}响应要能表达部分成功:
json
{
"succeeded": ["1001", "1002"],
"failed": [
{
"id": "1003",
"code": "USER_HAS_ACTIVE_ORDER",
"message": "用户存在未完成订单,不能删除"
}
]
}批量操作通常要考虑:
- 最大数量。
- 幂等性。
- 部分成功。
- 事务边界。
- 审计日志。
- 权限逐项校验。
23. 前后端协作中的接口契约
接口不是后端单方面写完给前端用。前后端应该先确认契约:
text
接口路径
HTTP 方法
请求参数
响应结构
错误格式
状态码
权限要求
分页/排序/筛选约定
字段枚举
兼容性规则推荐使用 OpenAPI:
yaml
openapi: 3.1.0
info:
title: User API
version: 1.0.0
paths:
/api/v1/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
responses:
"200":
description: User list
post:
summary: Create user
responses:
"201":
description: User createdOpenAPI 的价值:
- 生成接口文档。
- 生成前端类型。
- 生成 mock server。
- 做契约测试。
- 管理版本变更。
- 让 AI/Agent 工具更准确理解接口。
24. 后端分层建议
一个清晰的后端接口通常不应该把所有逻辑写在 Controller。
推荐分层:
text
Controller / Route
-> DTO / Schema validation
-> Service / Use case
-> Domain rules
-> Repository / DAO
-> Database / External service职责:
| 层 | 作用 |
|---|---|
| Controller | 解析 HTTP 请求、调用服务、返回响应 |
| DTO / Schema | 校验输入,做类型转换 |
| Service | 编排业务流程 |
| Domain | 核心业务规则 |
| Repository | 数据访问 |
| Integration | 外部 API、消息队列、对象存储 |
接口设计不只是 URL,后端内部结构也要支持接口长期演进。
25. 完整示例:用户和订单接口
25.1 用户接口
http
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/{userId}
PATCH /api/v1/users/{userId}
DELETE /api/v1/users/{userId}
GET /api/v1/users/{userId}/orders创建用户:
http
POST /api/v1/users
Content-Type: application/jsonjson
{
"username": "alice",
"email": "alice@example.com",
"displayName": "Alice"
}成功:
http
201 Created
Location: /api/v1/users/1001json
{
"id": "1001",
"username": "alice",
"email": "alice@example.com",
"displayName": "Alice",
"status": "active",
"createdAt": "2026-07-02T10:00:00Z"
}邮箱重复:
http
409 Conflictjson
{
"type": "https://api.example.com/problems/email-already-exists",
"title": "Email already exists",
"status": 409,
"detail": "The email alice@example.com is already used.",
"traceId": "req_1001"
}25.2 订单接口
http
GET /api/v1/orders
POST /api/v1/orders
GET /api/v1/orders/{orderId}
PATCH /api/v1/orders/{orderId}
POST /api/v1/orders/{orderId}/cancel
POST /api/v1/orders/{orderId}/payments
POST /api/v1/orders/{orderId}/refunds
GET /api/v1/orders/{orderId}/items创建订单:
http
POST /api/v1/orders
Idempotency-Key: idem_abc123
Content-Type: application/jsonjson
{
"items": [
{
"skuId": "sku_1001",
"quantity": 2
}
],
"receiverAddressId": "addr_1001"
}响应:
http
201 Created
Location: /api/v1/orders/order_1001json
{
"id": "order_1001",
"status": "pending_payment",
"amount": 59800,
"currency": "CNY",
"createdAt": "2026-07-02T10:00:00Z"
}取消订单:
http
POST /api/v1/orders/order_1001/cancel
Content-Type: application/jsonjson
{
"reason": "USER_REQUEST"
}如果订单已发货:
http
409 Conflictjson
{
"type": "https://api.example.com/problems/order-state-conflict",
"title": "Order state conflict",
"status": 409,
"detail": "Only pending_payment or paid orders can be cancelled.",
"traceId": "req_1002"
}26. AI / Agent 工具接口设计补充
如果后端接口会被 AI Agent 调用,接口设计要更严格,因为模型会根据描述自动选择工具。
要求:
- endpoint 和 operationId 命名清晰。
- 请求 schema 不要模糊。
- 错误消息必须告诉 Agent 如何修复。
- 大结果分页,不要一次返回几万行。
- 写操作要求幂等键。
- 高风险操作要求人工审批。
- OpenAPI 文档要完整,便于生成工具定义。
适合 Agent 的工具接口示例:
http
GET /api/v1/documents/search?q=rest&pageSize=5响应:
json
{
"data": [
{
"id": "doc_1001",
"title": "RESTful API 与后端接口设计学习文档",
"snippet": "URL 表示资源,HTTP 方法表示动作...",
"score": 0.92
}
]
}不适合:
http
POST /api/v1/doSomething因为 Agent 很难判断什么时候该调用、参数是什么、结果代表什么。
27. 常见错误
| 错误 | 问题 |
|---|---|
| 所有接口都用 POST | HTTP 语义丢失,缓存、幂等、网关策略都不好做 |
| 所有响应都返回 200 | 前端、监控、网关无法正确识别失败 |
| URL 里写动词 | 资源模型不清晰 |
| 接口直接暴露数据库表 | API 和数据库强耦合 |
| 错误格式不统一 | 前端处理困难,排查困难 |
| 不做资源级权限 | 容易出现越权访问 |
| 不限制分页大小 | 容易拖垮数据库 |
| 不考虑幂等 | 重试导致重复订单、重复扣款 |
| 不写 OpenAPI | 联调靠口头沟通,长期维护成本高 |
| 过度追求“纯 REST” | 业务动作表达不清,反而降低可维护性 |
28. REST 接口设计检查清单
设计接口前检查:
- [ ] 资源是否识别清楚?
- [ ] URL 是否使用名词和复数?
- [ ] HTTP 方法是否符合语义?
- [ ] 列表是否支持分页?
- [ ] 筛选和排序字段是否白名单?
- [ ] 创建接口是否考虑幂等?
- [ ] 更新接口是否考虑并发覆盖?
- [ ] 状态码是否准确?
- [ ] 错误响应是否统一?
- [ ] 是否有 traceId?
- [ ] 是否有认证和资源级授权?
- [ ] 是否限制请求体大小和分页大小?
- [ ] 是否需要缓存或明确禁止缓存?
- [ ] 是否需要 OpenAPI 文档?
- [ ] 是否有接口测试和契约测试?
29. 学习路线
建议按这个顺序学:
text
HTTP 基础
-> REST 资源建模
-> URL 和方法设计
-> 状态码和错误格式
-> 分页、筛选、排序
-> 认证和授权
-> 幂等和并发控制
-> 缓存和条件请求
-> OpenAPI 文档
-> 安全和限流
-> 项目接口设计实战练习项目:
- 用户管理 API。
- 订单管理 API。
- 角色权限 API。
- 文件上传下载 API。
- 异步导入导出任务 API。
- 给以上接口写一份 OpenAPI 文档。
30. 官方资料入口
| 主题 | 官方资料 |
|---|---|
| REST 原始论文 | Roy Fielding - Chapter 5: REST |
| HTTP 语义标准 | RFC 9110: HTTP Semantics |
| HTTP 方法 | MDN HTTP request methods |
| HTTP 状态码 | MDN HTTP response status codes |
| 资源导向 API 设计 | Google AIP-121 Resource-oriented design |
| REST API 设计指南 | Microsoft REST API Guidelines |
| OpenAPI | OpenAPI Specification |
| HTTP 错误格式 | RFC 9457: Problem Details for HTTP APIs |
| API 安全 | OWASP API Security Top 10 |
31. 一句话总结
RESTful API 设计的核心是:
text
用资源建模业务,
用 HTTP 方法表达动作,
用状态码表达结果,
用统一 JSON 契约表达数据和错误,
用权限、幂等、缓存、评测和 OpenAPI 让接口可维护、可协作、可上线。一个好的后端接口不是“能调通”就行,而是前端好用、后端好维护、网关好治理、监控好追踪、安全边界清楚、未来版本还能平滑演进。