大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
REST API
这一课你将能做到
- 按资源化思想为一套业务设计出规范的 RESTful URL,并用正确的 HTTP 方法表达增删改查
- 用 Express 实现一套带分页、校验、正确状态码的笔记 CRUD 接口,并用 curl 逐个验证
- 说出各方法的幂等性与安全性、常用状态码的语义,以及接口版本管理和文档的两种主流做法
为什么要学
第 4-6 课你掌握了存储能力,但数据躺在数据库里,前端拿不到——API 是后端交付产品的方式。接口一旦发布就是契约:前端照着对接、第三方照着集成、App 发出去的版本用户可能用好几年,改起来牵一发动全身。所以接口设计的能力(怎么命名、怎么用状态码、怎么演进)比接口数量重要得多。
人话:REST 不是框架不是库,是一套「用 URL 表示资源、用 HTTP 方法表示动作」的设计风格。GET /notes 是「看笔记列表」,DELETE /notes/3 是「删第 3 条」——看 URL 就懂意图,这就是 REST 追求的自解释性。
先建立图像
同一个需求,设计前 vs 设计后:
❌ RPC 风格(动作思维): ✅ REST 风格(资源思维):
GET /getNotes?id=1 GET /api/v1/notes 列表
GET /getNoteList GET /api/v1/notes/1 详情
POST /createNote POST /api/v1/notes 创建
POST /updateNote?id=1 PUT /api/v1/notes/1 全量更新
GET /deleteNote?id=1 PATCH /api/v1/notes/1 局部更新
DELETE /api/v1/notes/1 删除
对比三处关键差异:URL 是名词(资源)不是动词;动作由 HTTP 方法表达而不是路径;同一个资源 /notes/1 配不同方法就是不同操作。配合状态码,整张接口表不需要文档也能猜出七成。
跟着做
步骤 1:搭好 Express 骨架
mkdir bebg-api && cd bebg-api && npm init -y && npm install express
新建 server.js,先跑通第一个接口:
// server.js
const express = require('express');
const app = express();
app.use(express.json()); // 解析请求体里的 JSON(没有这行 req.body 是 undefined)
app.get('/api/v1/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000, () => console.log('http://localhost:3000'));
node server.js
curl http://localhost:3000/api/v1/health
# {"status":"ok"}
步骤 2:完成笔记 CRUD(先存内存,第 9 课接数据库)
把 server.js 扩充为完整接口:
// server.js
const express = require('express');
const app = express();
app.use(express.json());
let notes = []; // 演示用内存存储
let nextId = 1;
// 列表:支持分页与关键字过滤
app.get('/api/v1/notes', (req, res) => {
const page = Math.max(1, parseInt(req.query.page) || 1);
const size = Math.min(50, Math.max(1, parseInt(req.query.size) || 10));
const kw = (req.query.kw || '').trim();
let data = notes;
if (kw) data = data.filter(n => n.title.includes(kw));
const total = data.length;
data = data.slice((page - 1) * size, page * size);
res.json({ data, page, size, total });
});
// 详情
app.get('/api/v1/notes/:id', (req, res) => {
const note = notes.find(n => n.id === Number(req.params.id));
if (!note) return res.status(404).json({ error: '笔记不存在' });
res.json(note);
});
// 创建:201 + Location 头
app.post('/api/v1/notes', (req, res) => {
const { title, content } = req.body || {};
if (!title || !title.trim()) return res.status(400).json({ error: 'title 必填' });
const note = { id: nextId++, title: title.trim(), content: content || '', createdAt: new Date().toISOString() };
notes.push(note);
res.status(201).location(`/api/v1/notes/${note.id}`).json(note);
});
// 全量更新(PUT 语义:提交完整资源)
app.put('/api/v1/notes/:id', (req, res) => {
const note = notes.find(n => n.id === Number(req.params.id));
if (!note) return res.status(404).json({ error: '笔记不存在' });
const { title, content } = req.body || {};
if (!title || !title.trim()) return res.status(400).json({ error: 'title 必填' });
note.title = title.trim();
note.content = content || '';
res.json(note);
});
// 局部更新(PATCH 语义:只改提交的字段)
app.patch('/api/v1/notes/:id', (req, res) => {
const note = notes.find(n => n.id === Number(req.params.id));
if (!note) return res.status(404).json({ error: '笔记不存在' });
const { title, content } = req.body || {};
if (title !== undefined) {
if (!title.trim()) return res.status(400).json({ error: 'title 不能为空' });
note.title = title.trim();
}
if (content !== undefined) note.content = content;
res.json(note);
});
// 删除:204 无响应体
app.delete('/api/v1/notes/:id', (req, res) => {
const idx = notes.findIndex(n => n.id === Number(req.params.id));
if (idx === -1) return res.status(404).json({ error: '笔记不存在' });
notes.splice(idx, 1);
res.status(204).end();
});
// 统一 404(未匹配任何路由)
app.use((req, res) => res.status(404).json({ error: '接口不存在' }));
app.listen(3000, () => console.log('http://localhost:3000'));
步骤 3:用 curl 走完整个生命周期
# 创建
curl -i -X POST http://localhost:3000/api/v1/notes \
-H "Content-Type: application/json" \
-d '{"title":"第一条笔记","content":"REST 学习中"}'
# HTTP/1.1 201 Created,响应头里有 Location
# 列表(带分页与过滤)
curl "http://localhost:3000/api/v1/notes?page=1&size=5&kw=第一"
# 详情
curl -i http://localhost:3000/api/v1/notes/1
# 局部更新
curl -i -X PATCH http://localhost:3000/api/v1/notes/1 \
-H "Content-Type: application/json" -d '{"content":"改过的内容"}'
# 删除
curl -i -X DELETE http://localhost:3000/api/v1/notes/1
# HTTP/1.1 204 No Content
# 各种错误分支
curl -i http://localhost:3000/api/v1/notes/999 # 404
curl -i -X POST http://localhost:3000/api/v1/notes -H "Content-Type: application/json" -d '{}'
# 400,错误原因在响应体 JSON 里
重点观察 -i 输出的状态码行——前端、网关、监控全靠它判断结果。
步骤 4:接口文档——两种主流做法
做法 A:OpenAPI/Swagger 自动生成(FastAPI 内置,Express 加包):
npm install swagger-ui-express
在 server.js 挂载(文档定义节选,完整 spec 见延伸):
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = {
openapi: '3.0.0',
info: { title: 'Notes API', version: '1.0.0' },
paths: {
'/api/v1/notes': {
get: { summary: '笔记列表', parameters: [
{ name: 'page', in: 'query', schema: { type: 'integer', default: 1 } },
{ name: 'size', in: 'query', schema: { type: 'integer', default: 10 } },
] },
post: { summary: '创建笔记' },
},
},
};
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
浏览器打开 http://localhost:3000/docs 就有可调试的文档页——文档和代码同源,不会过期。
做法 B:Markdown 表格(小团队/内部接口够用):
| 方法 | 路径 | 说明 | 入参 | 成功响应 |
|---|---|---|---|---|
| GET | /api/v1/notes | 列表 | query: page,size,kw | 200 {data,page,size,total} |
| POST | /api/v1/notes | 创建 | body: title*,content | 201 note |
| PATCH | /api/v1/notes/:id | 局部更新 | body: title,content 任意组合 | 200 note |
| DELETE | /api/v1/notes/:id | 删除 | - | 204 |
讲清楚要点
- 方法的幂等性与安全性(面试重点):GET/PUT/DELETE 幂等(重复执行结果一样,所以网络重试安全),GET 还安全(不改变资源);POST 不幂等(重复提交会创建两条);PATCH 理论上幂等、按实现而定。理解幂等,才能解释「为什么浏览器刷新对 GET 无害,对下单接口危险」。
- 状态码按类记忆:2xx 成功(200 默认 / 201 已创建 / 204 已成功无内容);3xx 重定向(301 永久 / 302 临时 / 304 缓存协商);4xx 客户端错(400 参数错 / 401 未认证 / 403 无权限 / 404 不存在 / 409 冲突 / 429 太频繁);5xx 服务端错(500 内部错 / 502 网关收到无效响应 / 504 网关超时)。
- 错误响应要结构化:统一
{"error": "人话描述", "code": "NOTE_NOT_FOUND"}。code 给程序判断,message 给人看,别把堆栈直接丢给前端(泄漏内部信息)。 - 版本管理:路径版
/api/v1/...最直观最主流;变更规则——加字段不算破坏性,删字段/改语义/改结构是破坏性,必须升版本;旧版本给过渡期(响应头带Deprecation提示)。 - 集合资源的三个配套设计:分页(
?page=&size=,返回 total)、过滤(?kw=、?status=)、排序(?sort=-createdAt,负号表示倒序)。列表接口没有这三件套,数据量一涨必返工。
你可能会懵的地方
-
PUT 和 PATCH 到底差在哪? PUT 是「替换整个资源」——只传 content 会把 title 清掉(所以 PUT 里 title 必填);PATCH 是「改哪传哪」。拿不准就用:完整编辑表单用 PUT,勾选式开关/单个字段修改用 PATCH。
-
为什么 POST 创建成功是 201 不是 200? 201 Created 明确表达「不仅成功,而且新建了资源」,并配 Location 头告诉客户端新资源地址。语义准确的接口能让调用方少问「这接口是新建还是更新」。
-
国内很多接口全返 200 加 code 字段,错了吗? 是一种(不推荐的)流派:HTTP 层永远 200,业务码放 body。它让网关/监控/重试机制全部失效——人家看到 200 以为成功。除非维护历史系统,新项目按标准 HTTP 语义来。
-
URL 复数还是单数?嵌套多深合适? 复数是主流约定(/notes, /notes/1)。关联资源可以嵌套一层
/users/1/notes(某用户的笔记),再深就拆出来用查询参数表达(/notes?userId=1)。 -
REST 和 GraphQL 什么关系? REST 按「资源」组织多个端点,前端可能要调三次拼数据;GraphQL 一个端点由前端声明要什么字段。适合前端聚合需求复杂的场景。入门先把 REST 打牢,它仍是行业默认。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
URL 里写动词 /getNoteList | 方法语义被路径抢走,风格混乱 | 资源名词 + HTTP 方法表达动作 |
| 所有接口返回 200,错误码放 body | 监控/网关/重试全部失灵 | 状态码如实表达结果 |
| 创建成功返回 200 且不回 Location | 调用方拿不到新资源地址 | 201 + Location |
| 分页不做上限(size 传 10000) | 一次查询拖垮服务 | size 上限截断(如本例 50) |
| 返回堆栈或 SQL 报错原文 | 泄漏内部结构,有安全风险 | 统一错误结构 + 日志留在服务端 |
| 接口直接改字段含义不升版本 | 已上线的客户端集体崩 | 破坏性变更必升 /v2,v1 给过渡期 |
卡点急救
req.body是 undefined:忘了app.use(express.json()),或 curl 没带-H "Content-Type: application/json"。- 改了代码不生效:Node 不会热重载。Ctrl+C 重启,或
npm install --save-dev nodemon后用npx nodemon server.js。 Cannot GET /xxx:路径打错或方法不匹配;注意 Express 路由区分大小写和结尾斜杠(/notes/与/notes是两条)。- Windows PowerShell 里 curl 的
-d报错:PowerShell 会吃掉引号。用curl.exe并给 JSON 外层用单引号、内层转义双引号,或改用 Git Bash。 - 删除后 204 但想看响应体:204 语义就是无内容。想返回被删对象就改 200,二者选一,别 204 还塞 body。
面试常问
-
说一下 REST 是什么,它的约束有哪些? 表述性状态转移:资源化 URL、统一接口(HTTP 方法语义)、无状态(每个请求自带全部上下文,服务端不存会话)、分层系统、可缓存。能结合自己的接口举例最重要。
-
GET 和 POST 的区别? 语义:GET 取资源安全幂等,参数在 URL;POST 提交数据不幂等,参数在 body。实际影响:GET 可被缓存/收藏/预取,不能传敏感信息;POST 不进缓存与历史记录。
-
401 和 403 的区别?400 和 404 呢? 401 未认证(不知道你是谁,去登录);403 已认证但无权限(知道你是谁,但你不行)。400 参数/格式错误(请求本身不合法);404 资源不存在(请求合法但没这个资源)。
-
如何设计一个防止重复提交的接口? 方向:POST 天然不幂等,客户端生成唯一请求 ID(幂等键)放 header,服务端用 Redis
SET NX判重,重复请求返回第一次的结果;数据库层加唯一约束兜底。 -
接口文档为什么要和代码同源? 手写文档必然过期,过期的文档比没有更害人。OpenAPI 注解/装饰器从代码生成文档(FastAPI 自动生成、Spring 的 springdoc、Express 手动挂 spec),改代码即改文档。
自测三问
- 不查资料写出幂等性表格:六个常见 HTTP 方法各自安全吗、幂等吗?
- 把列表接口升级为「按状态过滤 + 按创建时间排序 + 游标分页」——写出 URL 与响应结构。
- 给本课 API 设计 v2:笔记支持多标签(数组字段),同时不能破坏 v1 客户端,说明你的迁移策略。
小结一句话
REST 的全部心法 = URL 只放资源、动作交给 HTTP 方法、结果交给状态码;配齐分页过滤排序与同源文档,接口就是一份前端不用问你的合同。
延伸
- MDN:HTTP 方法与状态码
- OpenAPI 规范
- 下一课:认证——接口谁都能调可不行,给它们加上门禁。