---
title: "7.3 让接口更好用"
description: "版本管理、接口文档、批量操作、文件上传、实时更新——把接口当产品来打磨"
chapter: "第七章"
---

# 7.3 让接口更好用

> **本节目标**：当接口越来越多、使用者越来越多时，了解怎么让接口从"能用"变成"好用"——文档、版本管理、批量操作、文件上传、实时更新。

---

## 小明的新烦恼

小明的"个人豆瓣"已经稳定运行了一个月。参数校验加了，错误处理统一了，连接池也配好了。他开始加新功能——收藏夹、分享链接、用户评论。

每加一个功能就多几个接口。收藏夹：添加收藏、取消收藏、获取收藏列表。评论：发评论、删评论、获取评论列表、点赞评论。分享：生成分享链接、获取分享统计。

小明数了数，已经有三十多个接口了。

然后他遇到了三个让他头疼的问题。

---

## 自己都记不清有哪些接口了

每次写前端代码，小明都要打开后端文件夹，一个个翻 `route.ts` 文件，确认"这个接口叫什么、要传什么参数、返回什么格式"。有时候他记错了参数名——把 `movieId` 写成了 `movie_id`，调了半天才发现是拼写问题。

更尴尬的是，他的朋友老王想基于他的电影数据做一个"年度观影报告"小程序。老王问："你的 API 文档在哪？我需要知道有哪些接口、怎么调用。"

小明愣住了——他没有文档。他只能打开微信，一个接口一个接口地告诉老王："电影列表是 GET /api/movies，支持 page、limit、tag、sort 这些参数……"发了二十多条消息，老王说："你就不能给我一个文档链接吗？"

### 让 AI 生成接口文档

你不需要手写文档。业界有一个标准格式叫 **OpenAPI**（以前叫 Swagger），专门用来描述 API 接口——每个接口的 URL、HTTP 方法、请求参数、响应格式、错误码，全部用结构化的方式定义。

生成 OpenAPI 文档后，可以用 **Swagger UI** 渲染成一个可交互的网页。这个网页不只是"看"——你可以直接在页面上填参数、点"发送"，实时测试每个接口。就像一个内置的 Postman。

这对你自己也有好处：不用再翻代码确认接口细节了，打开文档页面一目了然。

跟 AI 说：

> "帮我根据现有的所有 API Route 生成 OpenAPI 3.0 文档。然后加一个 `/api-docs` 页面用 Swagger UI 展示，开发环境可以直接在页面上测试接口。"

::: tip 文档和代码保持同步
最怕的是文档写好了，但后来改了接口忘了更新文档。解决方式是让文档从代码自动生成——每次接口代码变了，文档自动更新。跟 AI 说"用 next-swagger-doc 或类似工具，从 route.ts 文件自动生成 OpenAPI 文档"，它会帮你配好自动化流程。
:::

---

## 改了一个字段名，前端好几个页面崩了

小明觉得电影接口返回的 `score` 字段名不够好，想改成 `rating`。他让 AI 把后端的 `score` 全部替换成 `rating`，改完一跑——首页崩了、搜索页崩了、收藏页崩了。因为这三个页面的前端代码都在用 `movie.score` 取值，后端突然改成了 `rating`，前端找不到 `score` 字段，直接报 `undefined`。

"不就改了个字段名吗，怎么炸了这么多地方？"小明很郁闷。

老师傅说："接口一旦有人在用，改它就像改公共设施。你把路灯换了个位置，所有记住老位置的人晚上都会撞墙。"

### 向后兼容：改接口的安全姿势

**向后兼容**的意思是：你改了接口，但老的调用方式还能正常工作。新功能加上了，旧功能不受影响。

几个实用原则：

**加字段没事，删字段要小心。** 接口响应里新增一个 `rating` 字段，不会影响任何人——前端代码里没用到这个字段，它就静静地待在 JSON 里，不碍事。但删除 `score` 字段，所有用 `movie.score` 的地方都会崩。

**过渡期两个都保留。** 想把 `score` 改成 `rating`？正确的做法是：

1. 先在响应里**同时返回** `score` 和 `rating`（值一样）
2. 通知所有使用者（包括你自己的前端）："以后请用 `rating`，`score` 会在下个版本移除"
3. 等所有调用方都改完了，再删掉 `score`

这个过程叫**废弃（Deprecation）**。不是直接删，而是先标记为"即将废弃"，给使用者留出迁移时间。

**大改动用版本号。** 如果接口要做不兼容的大改（比如整个响应结构都变了），可以用 URL 版本号：

- `/api/v1/movies` —— 旧版接口，保持不变
- `/api/v2/movies` —— 新版接口，新的数据结构

老用户继续用 v1，新功能用 v2。等所有人都迁移到 v2 后，再下线 v1。

::: warning 个人项目需要版本管理吗？
如果只有你自己的前端在调接口，版本管理可以简化——前后端一起改就行。但如果有其他人在用你的 API（比如老王的小程序），或者你的应用有移动端（App 更新比网页慢，老版本 App 可能还在调旧接口），版本管理就很有必要了。

即使是个人项目，养成"加字段不删字段"的习惯也没坏处——它能避免很多"改了后端忘了改前端"的低级错误。
:::

跟 AI 说：

> "我要把电影接口的 score 字段改名为 rating。帮我做一个向后兼容的过渡：响应里同时返回 score 和 rating（值相同），并在响应头里加一个 Deprecation 标记。等我确认所有前端页面都改用 rating 后，再告诉我可以安全删除 score。"

---

## 一次删 50 条 vs 调 50 次删除接口

小明想做一个"批量删除"功能——用户在列表页勾选多部电影，点"删除选中"，一次性删掉。

最直觉的做法是前端循环调用：

```
for (const id of selectedIds) {
  await fetch(`/api/movies/${id}`, { method: 'DELETE' })
}
```

50 部电影，50 个 DELETE 请求。能跑，但有三个问题：

**慢。** 50 个请求意味着 50 次网络往返。即使每次只要 50ms，加起来也要 2.5 秒。用户点了"删除"后要等好几秒才能看到结果。

**不一致。** 删到第 30 个时网络断了，前 30 个删了，后 20 个没删。数据处于一个"半完成"的尴尬状态——用户以为全删了，实际上还剩 20 个。

**浪费资源。** 50 个请求占用 50 次数据库连接（虽然很短），在高并发时会加剧连接池的压力。

### 批量操作接口

更好的做法是提供一个**批量操作接口**：

`DELETE /api/movies/batch`

请求 body 里带上要删除的 ID 列表：`{ "ids": [1, 3, 5, 7, 9, ...] }`

后端在**一个数据库事务**里完成所有删除——要么全删成功，要么全部回滚，不会出现"删了一半"的情况。而且只有一次网络往返，速度快得多。

同样的思路适用于批量更新（比如"把选中的电影全部标记为已看"）和批量创建（比如"从 CSV 导入 100 部电影"）。

**判断标准：当你发现前端在循环调用同一个接口时，就该考虑是不是需要一个批量版本。**

跟 AI 说：

> "帮我加一个批量删除电影的接口 `DELETE /api/movies/batch`，接收 ID 数组（最多 100 个），在一个数据库事务里完成删除。如果任何一条删除失败（比如 ID 不存在），全部回滚并返回错误信息。"

::: warning 批量接口要限制数量
不要让用户一次删 10 万条——这会导致事务太大、锁表时间太长、甚至内存溢出。给批量接口加一个上限（比如一次最多 100 条），超过了让前端分批调用。
:::

---

## 用户想上传电影海报

小明想让用户给电影配一张海报图片。但他发现一个问题：之前所有接口都是收发 JSON 数据，图片是二进制文件，JSON 里放不下。

### multipart/form-data：传文件的方式

之前所有请求的 `Content-Type` 都是 `application/json`——请求体是一段 JSON 文本。但文件（图片、视频、PDF）是二进制数据，不能直接塞进 JSON 里。

**multipart/form-data** 是 HTTP 协议里专门用来传文件的格式。它可以在一个请求里同时传文本字段和二进制文件——比如电影标题（文本）和海报图片（文件）一起提交。

你不需要理解 multipart 的底层编码格式。只需要知道整个流程：

1. **前端**：用 `<input type="file">` 让用户选文件，然后用 `FormData` 对象把文件和其他字段打包，用 `fetch` 发送
2. **后端**：从请求里取出文件，做校验（大小、格式），存到某个地方，把文件的**相对路径**存到数据库
3. **前端**：拼上域名前缀，用完整 URL 显示图片

为什么存相对路径而不是完整 URL？因为域名可能会变——开发时是 `localhost:3000`，上线后是 `yourdomain.com`，迁移云存储后又变成 `cos.ap-shanghai.myqcloud.com`。如果数据库里存的是完整 URL，每次换域名都要批量更新数据库。存相对路径（比如 `/uploads/poster-abc123.jpg`），前端根据当前环境拼上域名前缀就行，数据库不用动。

当然，如果你的云存储域名已经稳定（比如绑了自定义域名），存完整 URL 也没问题——少一步拼接，用起来更直接。

### 文件存在哪？

文件存储有两种选择：

**本地存储**——存到服务器的文件系统里，比如 `public/uploads/` 目录。简单直接，开发阶段用这个就行。但上线后有问题：服务器磁盘空间有限，而且如果你用了 Serverless 部署（比如 Vercel），根本没有持久化的文件系统——每次部署文件都会丢失。

**云存储**——存到专门的文件存储服务，比如腾讯云 COS、Cloudflare R2、AWS S3、阿里云 OSS。文件存在云端，通过 URL 访问，不占用服务器资源。上线后应该用这个。

跟 AI 说（开发阶段）：

> "帮我加一个上传电影海报的功能。前端用文件选择器，后端接收图片后存到 public/uploads 目录，把图片路径存到 movies 表的 posterUrl 字段。限制文件大小 5MB 以内，只允许 jpg/png/webp 格式。上传前在前端预览图片。"

跟 AI 说（上线前）：

> "把文件上传从本地存储改成 Cloudflare R2（或你选的云存储服务）。上传后返回文件的公开访问 URL。"

::: warning 文件上传的安全注意事项
- **限制文件大小**：不限制的话，有人上传一个 1GB 的文件就能把你的服务器内存撑爆
- **限制文件类型**：只允许你需要的格式（图片就只允许 jpg/png/webp），不要接受 .exe、.sh 等可执行文件
- **不要用用户提供的文件名**：用户可能上传一个叫 `../../../etc/passwd` 的文件来搞路径穿越攻击。后端应该自己生成文件名（比如用 UUID）
:::

::: tip 统一上传入口，底层随时能换
不管你现在用本地存储还是云存储，跟 AI 说："把文件上传逻辑封装成一个统一的函数，开发阶段存本地，上线时只改这个函数的实现就能切换到云存储。"这样前端代码和业务逻辑完全不用动，只换存储层。

这就像家里的插座——不管你插的是台灯还是电扇，插座的接口是一样的。上传接口对前端来说永远是"传文件、拿 URL"，至于文件最终存在本地硬盘还是腾讯云 COS，前端不需要知道。
:::

---

## 什么时候需要实时更新

小明加了评论功能。用户 A 发了一条评论，用户 B 要刷新页面才能看到。小明想做成"不用刷新就能看到新评论"的效果——就像微信聊天一样，消息实时出现。

实时更新有三种方案，复杂度递增：

<RealtimeComparison />

### 轮询（Polling）

最简单的方案：前端每隔几秒自动请求一次接口，看有没有新数据。

```
每 10 秒：GET /api/movies/1/comments?since=上次请求的时间戳
```

好处是**实现极其简单**——就是一个 `setInterval` 加一个 `fetch`，不需要任何额外的基础设施。

坏处是**浪费资源**。大部分请求返回的都是"没有新数据"。如果有 1000 个用户同时在线，每 10 秒就是 100 个请求/秒，其中 99% 是无用的。

但对于小明的电影评论来说，轮询完全够用——评论不是高频操作，每 10-30 秒查一次新评论，用户体验完全可以接受。

### SSE（Server-Sent Events）

服务器主动推送数据给前端。前端建立一个长连接，服务器有新数据时主动推过来，不需要前端反复请求。

**单向的**：只有服务器能推，前端不能通过这个通道发消息。适合"通知推送""新评论提醒""订单状态更新"这类场景——服务器有新消息就推给你，你不需要回复。

好处是**实时性好、资源消耗低**——只有真正有新数据时才传输，没有无用请求。而且基于 HTTP，不需要额外的协议支持，部署简单。

### WebSocket

前端和服务器之间建立一个**持久的双向通道**，双方都能随时发消息。

适合**需要频繁双向通信**的场景——在线聊天、协同编辑（多人同时编辑一个文档）、实时游戏。这些场景里，不只是服务器要推数据给前端，前端也要频繁发数据给服务器。

坏处是**复杂度高**——需要处理连接断开重连、心跳检测、消息顺序保证等问题。部署也更复杂，需要支持 WebSocket 的服务器。

### 怎么选？

| 场景 | 推荐方案 | 理由 |
|------|---------|------|
| 评论列表、通知提醒 | 轮询或 SSE | 更新频率低，单向推送就够 |
| 数据看板（每分钟更新） | 轮询 | 更新频率低，轮询最简单 |
| 订单状态实时更新 | SSE | 服务器推送，不需要前端回复 |
| 在线聊天 | WebSocket | 需要双向实时通信 |
| 协同编辑 | WebSocket | 需要双向实时通信 + 冲突处理 |
| 实时游戏 | WebSocket | 需要极低延迟的双向通信 |

一个简单的判断标准：**如果只需要服务器推数据给前端，用 SSE。如果前端也需要频繁发数据给服务器，用 WebSocket。如果更新频率低（几秒到几分钟一次），轮询最省事。**

小明的评论功能，轮询就够了：

> "评论列表需要自动刷新，每 15 秒检查一次有没有新评论。用 SWR 的 refreshInterval 实现自动轮询，不需要手写 setInterval。只返回上次请求之后新增的评论。不需要 WebSocket。"

::: tip 为什么用 SWR 或 TanStack Query 而不是手写轮询
你跟 AI 说"做一个自动刷新的评论列表"，加载了 `vercel-react-best-practices` Skill 的 Claude Code 大概率会用 **SWR** 或 **TanStack Query**（以前叫 React Query）这类数据请求库。两者都自带请求去重（同一个页面多个组件用同一份数据，只发一次请求）、自动缓存、组件卸载时自动清理、失败自动重试。比手写 `setInterval + fetch` 省心得多，也不容易出"页面切走了但请求还在发"的 bug。

SWR 更轻量，API 更简洁，Vercel 团队维护。TanStack Query 功能更丰富，支持更复杂的缓存策略、乐观更新、无限滚动等场景。对于小明的评论轮询，两个都绰绰有余。你不需要了解它们的用法，AI 会根据项目情况选一个合适的——看到它们出现在代码里不用觉得奇怪。
:::

---

## API 即产品

回顾这一节讲的所有内容，有一个共同的思维转变：**接口不只是"后端的事"，它是前端（和其他消费者）的"产品"**。

好的接口像好的产品一样：

- **有文档**——使用者知道怎么用，不需要翻代码或问人
- **向后兼容**——升级不会坑老用户，改动有过渡期
- **批量操作**——效率高，不让使用者做重复劳动
- **错误信息清晰**——出问题知道是谁的问题、怎么修
- **支持多种数据格式**——不只是 JSON，还能处理文件上传
- **按需实时**——该实时的实时，不该实时的别浪费资源

你不需要一开始就做到完美。先跑通 CRUD（7.0），遇到数据量和需求增长的问题就加分页和过滤（7.1），上线后遇到脏数据和错误就加校验和错误处理（7.2），接口多了就加文档和版本管理（7.3）——这就是从"能跑"到"好用"的完整路径。

每一步都是被真实问题推动的，不是提前过度设计的。这也是 Vibe Coding 的核心理念：**先让它跑起来，再让它跑得好。**

---

::: tip 本节核心要点

- **接口文档**：用 OpenAPI/Swagger 自动生成，自己查方便，别人用也有据可查
- **向后兼容**：加字段没事，删字段要过渡；大改动用版本号
- **批量操作**：循环调接口 → 改成批量接口，用事务保证一致性
- **文件上传**：开发用本地存储，上线用云存储；注意限制大小和类型
- **实时更新**：轮询最简单，SSE 适合单向推送，WebSocket 适合双向通信
:::

---

::: info 下一步
API 搭好了，但现在谁都能调你的接口——没有登录、没有权限控制。去 [第八章：谁能访问我的数据](../08-auth-security/index.md) 学习认证和安全。
:::
