# 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 请求-响应流程:
---
## 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`
`POST /createOrder` | `GET /users`
`POST /orders` | URL 是资源地址,HTTP 方法已表达操作 |
| **用复数形式** | `GET /user`
`GET /order` | `GET /users`
`GET /users/123` | 统一用复数,避免 `/user` 和 `/users` 混用 |
| **小写+连字符** | `GET /UserProfiles`
`GET /user_profiles` | `GET /user-profiles`
`GET /order-items` | URL 大小写敏感,统一小写最安全 |
| **避免层级过深** | `GET /users/123/orders/456/items/789` | `GET /users/123/orders`
`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 常用状态码演示
👇 **动手试试看**:点击下方按钮,了解常见状态码的含义:
---
## 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 正确的错误处理演示
👇 **动手试试看**:对比"好的"和"差的"错误响应设计:
---
## 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 版本控制的策略和最佳实践:
---
## 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 | 验证"你能做什么"(权限) |