REST API
JSON 与 REST 设计。
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'));