CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
后端入门实战/课时/REST API
RawBlame
课时目录12 节

后端入门实战

  1. 01后端入门概览
  2. 02Git 与托管
  3. 03语言选型
  4. 04PostgreSQL
  5. 05数据库进阶
  6. 06Redis 与缓存
  7. 07REST API
  8. 08认证
  9. 09ORM
  10. 10单元测试
  11. 11集成与功能测试
  12. 12综合练习

REST API

JSON 与 REST 设计。

30 分钟

REST API

这一课你将能做到

  1. 按资源化思想为一套业务设计出规范的 RESTful URL,并用正确的 HTTP 方法表达增删改查
  2. 用 Express 实现一套带分页、校验、正确状态码的笔记 CRUD 接口,并用 curl 逐个验证
  3. 说出各方法的幂等性与安全性、常用状态码的语义,以及接口版本管理和文档的两种主流做法

为什么要学

第 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,kw200 {data,page,size,total}
POST/api/v1/notes创建body: title*,content201 note
PATCH/api/v1/notes/:id局部更新body: title,content 任意组合200 note
DELETE/api/v1/notes/:id删除-204

讲清楚要点

  1. 方法的幂等性与安全性(面试重点):GET/PUT/DELETE 幂等(重复执行结果一样,所以网络重试安全),GET 还安全(不改变资源);POST 不幂等(重复提交会创建两条);PATCH 理论上幂等、按实现而定。理解幂等,才能解释「为什么浏览器刷新对 GET 无害,对下单接口危险」。
  2. 状态码按类记忆:2xx 成功(200 默认 / 201 已创建 / 204 已成功无内容);3xx 重定向(301 永久 / 302 临时 / 304 缓存协商);4xx 客户端错(400 参数错 / 401 未认证 / 403 无权限 / 404 不存在 / 409 冲突 / 429 太频繁);5xx 服务端错(500 内部错 / 502 网关收到无效响应 / 504 网关超时)。
  3. 错误响应要结构化:统一 {"error": "人话描述", "code": "NOTE_NOT_FOUND"}。code 给程序判断,message 给人看,别把堆栈直接丢给前端(泄漏内部信息)。
  4. 版本管理:路径版 /api/v1/... 最直观最主流;变更规则——加字段不算破坏性,删字段/改语义/改结构是破坏性,必须升版本;旧版本给过渡期(响应头带 Deprecation 提示)。
  5. 集合资源的三个配套设计:分页(?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 给过渡期

卡点急救

  1. req.body 是 undefined:忘了 app.use(express.json()),或 curl 没带 -H "Content-Type: application/json"。
  2. 改了代码不生效:Node 不会热重载。Ctrl+C 重启,或 npm install --save-dev nodemon 后用 npx nodemon server.js。
  3. Cannot GET /xxx:路径打错或方法不匹配;注意 Express 路由区分大小写和结尾斜杠(/notes/ 与 /notes 是两条)。
  4. Windows PowerShell 里 curl 的 -d 报错:PowerShell 会吃掉引号。用 curl.exe 并给 JSON 外层用单引号、内层转义双引号,或改用 Git Bash。
  5. 删除后 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),改代码即改文档。

自测三问

  1. 不查资料写出幂等性表格:六个常见 HTTP 方法各自安全吗、幂等吗?
  2. 把列表接口升级为「按状态过滤 + 按创建时间排序 + 游标分页」——写出 URL 与响应结构。
  3. 给本课 API 设计 v2:笔记支持多标签(数组字段),同时不能破坏 v1 客户端,说明你的迁移策略。

小结一句话

REST 的全部心法 = URL 只放资源、动作交给 HTTP 方法、结果交给状态码;配齐分页过滤排序与同源文档,接口就是一份前端不用问你的合同。

延伸

  • MDN:HTTP 方法与状态码
  • OpenAPI 规范
  • 下一课:认证——接口谁都能调可不行,给它们加上门禁。
加载本节练习…
← 上一课Redis 与缓存下一课 →认证