Skip to content

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 的部分字段
删除用户 1001

REST 的关键不是“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/users

RPC 更偏动作:

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 0The Swamp of POX只有一个 endpoint,全用 POST 传动作POST /api/do
Level 1Resources每个资源有独立 URLGET /users/1、GET /orders/2
Level 2HTTP Verbs用 HTTP 方法表达动作,配合状态码POST /users、DELETE /users/1
Level 3Hypermedia 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}/refunds

5. 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
/productInfo

5.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}/items

5.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-jobs

POST 默认不是幂等的。网络重试可能造成重复创建,所以创建订单、支付、提交表单时要考虑幂等键。

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_123

7.2 客户端错误状态码 ​

状态码场景
400 Bad Request请求语法或基础参数错误
401 Unauthorized未认证或认证失效
403 Forbidden已认证但无权限
404 Not Found资源不存在
409 Conflict当前资源状态冲突
412 Precondition Failed条件请求失败,例如 ETag 不匹配
415 Unsupported Media Type请求体格式不支持
422 Unprocessable ContentJSON 格式正确,但业务语义校验失败
429 Too Many Requests触发限流

常见选择:

text
字段缺失、JSON 格式错 -> 400
没登录 -> 401
没权限 -> 403
查不到资源 -> 404
重复提交、状态不允许 -> 409
业务字段校验失败 -> 422

7.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-02

8.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/1001
json
{
  "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_1001
json
{
  "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-search
json
{
  "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}/items

12.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/permissions
json
{
  "permissionIds": ["user:read", "user:write", "order:read"]
}

如果是添加一个权限,可以用 POST:

http
POST /api/v1/roles/admin/permissions/user:read

13. 业务动作怎么设计 ​

不是所有业务都能自然映射成 CRUD。遇到动作时有三种设计方式。

13.1 作为资源状态更新 ​

修改订单状态:

http
PATCH /api/v1/orders/{orderId}
json
{
  "status": "cancelled"
}

适合简单状态修改。

13.2 作为子资源创建 ​

退款是一种独立业务记录:

http
POST /api/v1/orders/{orderId}/refunds
json
{
  "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_abc123
json
{
  "productId": "prod_1001",
  "quantity": 1
}

后端处理:

text
同一用户 + 同一 Idempotency-Key + 同一接口
-> 第一次执行并保存结果
-> 后续重复请求直接返回第一次结果

适用场景:

  • 创建订单。
  • 支付。
  • 提交审批。
  • 导入任务。
  • 发送通知。
  • 任何可能因重试造成重复副作用的 POST。

14.2 前端防抖不是幂等 ​

按钮 loading、防重复点击只是用户体验层。真正的幂等必须在服务端实现。

14.3 Idempotency-Key 的内部机制 ​

Idempotency-Key 不是简单「查一下有没有重复」就完了,服务端要做三件事:

  1. 唯一性范围:幂等键必须限定在「同一用户 + 同一接口」范围内才有意义。两个不同用户各自传 idem_abc123 不该互相冲突;用户 A 在 POST /orders 和 POST /refunds 传同一个键,也应该被视为两个不同操作。实现上通常用 (userId, endpoint, key) 作为数据库唯一索引。

  2. 结果存储:第一次请求成功后,把完整的响应(状态码 + 响应体)连同键一起存下来,设置一个过期时间(常见 24 小时)。后续重复请求直接返回缓存的响应,而不是重新执行。⚠️ 只存「已处理过」这个标志而不存响应体,会导致重试时返回不了第一次的结果,破坏幂等语义。

  3. 并发冲突:两个携带相同键的请求同时到达时,唯一索引会拒绝其中一个。被拒绝的请求不该直接返回 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=60

16.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 Unauthorized

17.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-jobs
json
{
  "type": "orders",
  "filters": {
    "status": "paid"
  }
}

响应:

http
202 Accepted
Location: /api/v1/export-jobs/job_1001
json
{
  "id": "job_1001",
  "status": "queued"
}

查询任务:

http
GET /api/v1/export-jobs/job_1001
json
{
  "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
cancelled

21. 文件上传和下载接口 ​

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-sessions
json
{
  "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:batchDelete
json
{
  "ids": ["1001", "1002", "1003"]
}

或者:

http
POST /api/v1/batch-operations
json
{
  "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 created

OpenAPI 的价值:

  • 生成接口文档。
  • 生成前端类型。
  • 生成 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/json
json
{
  "username": "alice",
  "email": "alice@example.com",
  "displayName": "Alice"
}

成功:

http
201 Created
Location: /api/v1/users/1001
json
{
  "id": "1001",
  "username": "alice",
  "email": "alice@example.com",
  "displayName": "Alice",
  "status": "active",
  "createdAt": "2026-07-02T10:00:00Z"
}

邮箱重复:

http
409 Conflict
json
{
  "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/json
json
{
  "items": [
    {
      "skuId": "sku_1001",
      "quantity": 2
    }
  ],
  "receiverAddressId": "addr_1001"
}

响应:

http
201 Created
Location: /api/v1/orders/order_1001
json
{
  "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/json
json
{
  "reason": "USER_REQUEST"
}

如果订单已发货:

http
409 Conflict
json
{
  "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. 常见错误 ​

错误问题
所有接口都用 POSTHTTP 语义丢失,缓存、幂等、网关策略都不好做
所有响应都返回 200前端、监控、网关无法正确识别失败
URL 里写动词资源模型不清晰
接口直接暴露数据库表API 和数据库强耦合
错误格式不统一前端处理困难,排查困难
不做资源级权限容易出现越权访问
不限制分页大小容易拖垮数据库
不考虑幂等重试导致重复订单、重复扣款
不写 OpenAPI联调靠口头沟通,长期维护成本高
过度追求“纯 REST”业务动作表达不清,反而降低可维护性

28. REST 接口设计检查清单 ​

设计接口前检查:

  • [ ] 资源是否识别清楚?
  • [ ] URL 是否使用名词和复数?
  • [ ] HTTP 方法是否符合语义?
  • [ ] 列表是否支持分页?
  • [ ] 筛选和排序字段是否白名单?
  • [ ] 创建接口是否考虑幂等?
  • [ ] 更新接口是否考虑并发覆盖?
  • [ ] 状态码是否准确?
  • [ ] 错误响应是否统一?
  • [ ] 是否有 traceId?
  • [ ] 是否有认证和资源级授权?
  • [ ] 是否限制请求体大小和分页大小?
  • [ ] 是否需要缓存或明确禁止缓存?
  • [ ] 是否需要 OpenAPI 文档?
  • [ ] 是否有接口测试和契约测试?

29. 学习路线 ​

建议按这个顺序学:

text
HTTP 基础
-> REST 资源建模
-> URL 和方法设计
-> 状态码和错误格式
-> 分页、筛选、排序
-> 认证和授权
-> 幂等和并发控制
-> 缓存和条件请求
-> OpenAPI 文档
-> 安全和限流
-> 项目接口设计实战

练习项目:

  1. 用户管理 API。
  2. 订单管理 API。
  3. 角色权限 API。
  4. 文件上传下载 API。
  5. 异步导入导出任务 API。
  6. 给以上接口写一份 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
OpenAPIOpenAPI Specification
HTTP 错误格式RFC 9457: Problem Details for HTTP APIs
API 安全OWASP API Security Top 10

31. 一句话总结 ​

RESTful API 设计的核心是:

text
用资源建模业务,
用 HTTP 方法表达动作,
用状态码表达结果,
用统一 JSON 契约表达数据和错误,
用权限、幂等、缓存、评测和 OpenAPI 让接口可维护、可协作、可上线。

一个好的后端接口不是“能调通”就行,而是前端好用、后端好维护、网关好治理、监控好追踪、安全边界清楚、未来版本还能平滑演进。

基于 VitePress 构建的个人学习文档知识库