Files
test-repo/docs/zh-cn/appendix/4-server-and-backend/api-design.md
T

318 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API 设计:前后端的"对话协议"
::: tip 🎯 核心问题
**前后端如何高效对话?** 这就像问:餐厅的菜单怎么设计,客人一看就懂?服务员怎么记单,不会出错?上菜怎么规范,客人满意?API 设计解决的就是"对话规则"的问题。
:::
---
## 0. 先问一个问题:你有没有经历过这些噩梦?
**场景一:接口命名随心所欲**
```
GET /getUserData
GET /fetchUserInfo
GET /queryUserById
GET /users/query
```
四个接口,功能一样,命名风格完全不同。新人入职一脸懵:我该用哪个?
**场景二:错误处理五花八门**
```json
// 有的返回 HTTP 状态码
HTTP/1.1 404 Not Found
// 有的返回 200 + code
HTTP/1.1 200 OK
{ "code": 404, "message": "用户不存在" }
// 有的直接抛异常
HTTP/1.1 200 OK
{ "error": "出错了" }
```
前端不知道该怎么判断请求是否成功。
**场景三:响应结构千人千面**
```json
// 接口 A
{ "data": { ... } }
// 接口 B
{ "result": { ... } }
// 接口 C
{ "content": { ... } }
```
每个接口返回格式都不一样,前端需要针对每个接口单独处理。
---
**好的 API 设计就像餐厅的点餐系统**——菜单清晰、流程规范、出错有提示。
---
## 1. 什么是 API
**API**Application Programming Interface,应用程序编程接口)就是"程序之间对话的约定"。
### 1.1 用餐厅来类比
| 餐厅角色 | 对应概念 | 说明 |
| :--- | :--- | :--- |
| 菜单 | API 文档 | 告诉你有哪些"菜"可以点 |
| 服务员 | HTTP 协议 | 标准化的"对话方式" |
| 后厨 | 服务端 | 按"订单"处理请求 |
| 上菜 | 响应 | 把结果返回给"客人" |
### 1.2 一个完整的 API 请求
👇 **动手试试看**:点击下方按钮,观察一次完整的 API 请求-响应流程:
<ApiRequestDemo />
---
## 2. RESTful 设计:让 URL 会说话
**REST**Representational State Transfer)是一种架构风格,核心思想是:
- 把网络上的事物抽象为"资源"Resource
- 用 URL 标识资源
- 用 HTTP 方法操作资源
### 2.1 用仓库来类比
| 仓库概念 | REST 对应 | 示例 |
| :--- | :--- | :--- |
| 货架地址 | URL | `/users``/orders` |
| 操作方式 | HTTP 方法 | GET(查看)、POST(入库) |
| 货物 | 资源 | 用户数据、订单数据 |
**关键原则**URL 是名词,不是动词。
### 2.2 URL 设计规则
| 规则 | 错误示例 | 正确示例 | 说明 |
| :--- | :--- | :--- | :--- |
| **用名词不用动词** | `GET /getUsers`<br>`POST /createOrder` | `GET /users`<br>`POST /orders` | URL 是资源地址,HTTP 方法已表达操作 |
| **用复数形式** | `GET /user`<br>`GET /order` | `GET /users`<br>`GET /users/123` | 统一用复数,避免 `/user``/users` 混用 |
| **小写+连字符** | `GET /UserProfiles`<br>`GET /user_profiles` | `GET /user-profiles`<br>`GET /order-items` | URL 大小写敏感,统一小写最安全 |
| **避免层级过深** | `GET /users/123/orders/456/items/789` | `GET /users/123/orders`<br>`GET /order-items/789` | 超过 3 层考虑重构,用扁平化路径 |
| **过滤用查询参数** | `GET /products/category/phone/price/5000` | `GET /products?category=phone&price_max=5000` | 过滤、排序、分页都用查询参数 |
::: tip 💡 URL 大小写敏感
统一用小写 + 连字符(-)是最安全的做法,避免大小写混乱和下划线风格不一致的问题。
:::
### 2.3 HTTP 方法选择
| 方法 | 用途 | 幂等性 | 安全性 | 典型场景 |
| :--- | :--- | :--- | :--- | :--- |
| **GET** | 获取资源 | 是 | 是 | 查询列表、查看详情 |
| **POST** | 创建资源 | 否 | 否 | 新增用户、提交订单 |
| **PUT** | 全量更新 | 是 | 否 | 替换整个用户资料 |
| **PATCH** | 部分更新 | 否 | 否 | 只修改昵称 |
| **DELETE** | 删除资源 | 是 | 否 | 删除用户、取消订单 |
::: tip 💡 什么是幂等性?
**幂等性**:多次执行结果相同。
- **幂等的操作**GET/PUT/DELETE):点 10 次和点 1 次,结果一样
- **不幂等的操作**(POST):点 10 次,可能创建 10 个订单
**解决方案**:POST 操作用唯一 ID 校验,避免重复处理。
:::
---
## 3. 状态码:让错误"会说话"
HTTP 状态码是服务器告诉客户端"发生了什么"的标准方式。
### 3.1 状态码分类
| 分类 | 含义 | 典型状态码 |
| :--- | :--- | :--- |
| **2xx** | 成功 | 200 OK、201 Created、204 No Content |
| **3xx** | 重定向 | 301 永久移动、304 未修改 |
| **4xx** | 客户端错误 | 400 参数错误、401 未认证、404 不存在 |
| **5xx** | 服务端错误 | 500 内部错误、503 服务不可用 |
### 3.2 常用状态码演示
👇 **动手试试看**:点击下方按钮,了解常见状态码的含义:
<StatusCodeDemo />
---
## 4. 错误处理:优雅地"拒绝"
好的错误处理能让客户端"看状态码就知道怎么回事",而不是去猜。
### 4.1 错误处理的"避坑指南"
**坑 1:所有错误都返回 200**
```json
// ❌ 错误做法
HTTP/1.1 200 OK
{ "error": "出错了" }
```
问题:缓存层会缓存这个"成功"响应,监控系统发现不了问题。
**坑 2:错误信息太笼统**
```json
// ❌ 错误做法
HTTP/1.1 400 Bad Request
{ "message": "参数错误" }
```
问题:客户端不知道哪个参数错了、为什么错。
**坑 3:暴露敏感信息**
```json
// ❌ 危险做法
HTTP/1.1 500 Internal Server Error
{ "stack": "at UserService.login...", "sql": "SELECT * FROM..." }
```
危险:暴露了代码结构、数据库查询,攻击者可以利用这些信息。
### 4.2 正确的错误处理演示
👇 **动手试试看**:对比"好的"和"差的"错误响应设计:
<ErrorHandlingDemo />
---
## 5. 版本控制:API 的"向后兼容"
### 5.1 为什么要版本控制?
场景:你的 App 有 100 万用户,需要修改订单接口。
**如果不做版本控制**
- 新 App 调用新接口 → 正常
- 旧 App 调用新接口 → 字段缺失,崩溃!
**正确的做法**
- `/v1/orders` - 旧接口,继续服务旧 App
- `/v2/orders` - 新接口,新功能在这里
### 5.2 版本控制策略
| 策略 | 示例 | 优点 | 缺点 |
| :--- | :--- | :--- | :--- |
| **URL 路径** | `/v1/users` | 直观、易缓存 | URL 变长 |
| **请求头** | `Accept: vnd.api.v2+json` | URL 干净 | 不便调试 |
| **查询参数** | `/users?version=2` | 简单 | 不够标准 |
### 5.3 版本控制演示
👇 **动手试试看**:了解 API 版本控制的策略和最佳实践:
<ApiVersioningDemo />
---
## 6. 响应结构:标准化的"数据契约"
无论成功还是失败,响应结构应该保持一致:
### 6.1 标准响应格式
```json
{
"code": 0,
"message": "success",
"data": { ... },
"request_id": "req-550e8400",
"timestamp": "2024-01-15T09:30:00.000Z"
}
```
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `code` | number | 业务状态码,0 表示成功 |
| `message` | string | 状态描述 |
| `data` | any | 业务数据 |
| `request_id` | string | 请求唯一标识,用于问题追踪 |
| `timestamp` | string | 响应时间戳 |
### 6.2 分页响应格式
```json
{
"code": 0,
"data": {
"items": [...],
"pagination": {
"page": 1,
"page_size": 20,
"total": 156,
"total_pages": 8
}
}
}
```
::: tip 💡 为什么要 request_id
**request_id** 是问题追踪的关键:
1. 用户反馈:"支付失败,错误 ID 是 abc123"
2. 技术人员直接在日志里搜索 abc123,立即定位问题
3. 分布式系统中,每个服务都记录相同的 request_id,可以把所有相关日志聚合起来
:::
---
## 7. 实战:电商系统 API 设计示例
```
# 用户模块
GET /v1/users # 获取用户列表
POST /v1/users # 创建新用户
GET /v1/users/{id} # 获取用户详情
PUT /v1/users/{id} # 全量更新用户
PATCH /v1/users/{id} # 部分更新用户
DELETE /v1/users/{id} # 删除用户
# 订单模块
GET /v1/users/{id}/orders # 获取某用户的订单
POST /v1/orders # 创建订单
GET /v1/orders/{id} # 获取订单详情
PATCH /v1/orders/{id}/status # 更新订单状态
# 商品模块(复杂过滤用查询参数)
GET /v1/products?category=phone&price_max=5000&sort=price_desc&page=1
```
---
## 名词速查表
| 名词 | 英文 | 解释 |
| :--- | :--- | :--- |
| **API** | Application Programming Interface | 程序之间对话的约定 |
| **REST** | Representational State Transfer | 一种架构风格,用 URL 标识资源 |
| **资源** | Resource | REST 架构的核心概念,有唯一标识(URL) |
| **幂等性** | Idempotency | 多次执行结果相同 |
| **状态码** | Status Code | HTTP 协议定义的响应状态 |
| **版本控制** | Versioning | 让新旧 API 并存,平滑升级 |
| **请求体** | Request Body | POST/PUT/PATCH 请求携带的数据 |
| **响应体** | Response Body | 服务器返回的数据 |
| **Header** | Header | 请求/响应的元数据(如 Content-Type |
| **认证** | Authentication | 验证"你是谁"(登录、Token |
| **授权** | Authorization | 验证"你能做什么"(权限) |