第4章 4.5 综合实战:个人博客 API
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了 Express 的基本用法,会用 app.get() 接收请求、用 res.json() 返回数据了。但说实话,那些例子都是"Hello World"级别的——真实项目里,你总不能只返回一个固定字符串吧?
你有没有遇到过这些痛苦:
- 想做个博客系统,不知道怎么把文章数据存起来、怎么让人"增删改查"
- 网上教程看了一堆,
router.get('/:id')这种语法看懂了,但不知道怎样才能"真正跑起来" - 想加个"登录才能发文章"的功能,不知道从哪里下手
这一章,我们把这些全都接起来——用 Express 做一整套个人博客 API,文章能发、能看、能改、能删,还能加个简单的token验证。
学完你会有一个"能跑的个人博客后端",下一章我们再给它接上 MySQL 数据库,让数据能真正持久保存。
🧱 基础 25 分钟:核心概念(小白视角)
4.5.1 什么是 REST API?——"餐厅点菜"的比喻
在继续写代码之前,先把一个重要概念讲清楚:REST API。
类比时间到!
想象你去一家餐厅:
- 你(客户端)跟服务员说:"我要一份宫保鸡丁,微辣"
- 服务员(服务器)把你的请求传到后厨
- 做好了,服务员端盘子上来(响应)
REST API 就是一套"点菜规则":
- GET = 问服务员"有什么菜?"(查)
- POST = 点新菜(增)
- PUT = 跟服务员说"帮我换一份,不要辣了"(改)
- DELETE = "这份不要了,退掉"(删)
Express 里对应就是:
app.get('/articles', ...) // 获取文章列表
app.post('/articles', ...) // 创建新文章
app.put('/articles/:id', ...) // 修改某篇文章
app.delete('/articles/:id', ...) // 删除某篇文章

4.5.2 路由参数——URL里的变量
有时候 URL 里要带个 ID,比如 /articles/123,表示"第123篇文章"。
Express 里用冒号 : 表示这是变量:
// 冒号后面的 id 是变量
app.get('/articles/:id', (req, res) => {
// req.params.id 就能拿到 URL 里的 123
res.json({ id: req.params.id, title: '示例文章' })
})
请求 GET /articles/456,返回 {"id":"456","title":"示例文章"}
一行代码解释:req.params 是个对象,存着 URL 里的所有参数。
4.5.3 中间件——"安检门"的比喻
类比时间!
中间件就像机场的安检门:
- 你(请求)进去,先过安检(中间件)
- 安检过了,才能登机(到达真正的路由处理函数)
- 安检发现你带打火机,直接拦下(返回错误)
Express 用 app.use() 或 app.all() 挂载中间件:
// 这是一个简单的日志中间件
app.use((req, res, next) => {
console.log(`收到请求: ${req.method} ${req.url}`)
next() // 放行,让请求继续往下走
})
// 这个路由才会被执行
app.get('/articles', (req, res) => {
res.json([{ title: '我的博客' }])
})
一句话解释:next() 非常重要,没有它,请求就卡在中间件里出不来!
4.5.4 用 express.json() 解析请求体
客户端发 POST/PUT 请求时,数据放在请求体(body)里。Express 默认不认识 JSON,需要用中间件解析:
// 加上这行,req.body 才有数据
app.use(express.json())
app.post('/articles', (req, res) => {
// req.body 里面就是客户端发来的 JSON 数据
const { title, content } = req.body
res.json({ message: '创建成功', data: { title, content } })
})
一行代码解释:express.json() 是帮我们把 JSON 格式的请求体,变成 JavaScript 对象。

🔥 实战 35 分钟:3 个递进的小项目
📦 项目 1(5分钟):博客的"骨架"——能跑起来的最小 API
先搭一个最基础的结构,跑起来再说。
完整代码:
// 1_create_server.js
const express = require('express')
const app = express()
const PORT = 3000
// 解析 JSON 请求体
app.use(express.json())
// 模拟的文章数据(暂时存在内存里)
let articles = [
{ id: 1, title: '第一篇文章', content: '这是内容' },
{ id: 2, title: '第二篇文章', content: '另一篇内容' }
]
// GET 获取所有文章
app.get('/articles', (req, res) => {
res.json(articles)
})
// 启动服务器
app.listen(PORT, () => {
console.log(`博客 API 已启动,访问 http://localhost:${PORT}`)
})
运行:
node 1_create_server.js
预期输出:
博客 API 已启动,访问 http://localhost:3000
测试(浏览器或 Postman 访问):
GET http://localhost:3000/articles
返回:
[{"id":1,"title":"第一篇文章","content":"这是内容"},{"id":2,"title":"第二篇文章","content":"另一篇内容"}]
一句话解释:这就是我们的"数据库",虽然只是内存数组,但足够先跑起来了。
📦 项目 2(15分钟):完整的 CRUD——增删改查全搞定
在项目1基础上,加上创建、修改、删除功能。
完整代码:
// 2_blog_crud.js
const express = require('express')
const app = express()
const PORT = 3000
app.use(express.json())
// 模拟数据库(内存)
let articles = [
{ id: 1, title: '第一篇文章', content: '这是内容' },
{ id: 2, title: '第二篇文章', content: '另一篇内容' }
]
let nextId = 3 // 下一个新文章的 ID
// ========== 增删改查 4 个路由 ==========
// 1. GET 获取所有文章
app.get('/articles', (req, res) => {
res.json(articles)
})
// 2. GET 获取单篇文章(根据 ID)
app.get('/articles/:id', (req, res) => {
const id = parseInt(req.params.id)
const article = articles.find(a => a.id === id)
if (!article) {
return res.status(404).json({ error: '文章不存在' })
}
res.json(article)
})
// 3. POST 创建新文章
app.post('/articles', (req, res) => {
const { title, content } = req.body
if (!title || !content) {
return res.status(400).json({ error: '标题和内容不能为空' })
}
const newArticle = { id: nextId++, title, content }
articles.push(newArticle)
res.status(201).json({ message: '创建成功', article: newArticle })
})
// 4. PUT 修改文章
app.put('/articles/:id', (req, res) => {
const id = parseInt(req.params.id)
const article = articles.find(a => a.id === id)
if (!article) {
return res.status(404).json({ error: '文章不存在' })
}
const { title, content } = req.body
if (title) article.title = title
if (content) article.content = content
res.json({ message: '更新成功', article })
})
// 5. DELETE 删除文章
app.delete('/articles/:id', (req, res) => {
const id = parseInt(req.params.id)
const index = articles.findIndex(a => a.id === id)
if (index === -1) {
return res.status(404).json({ error: '文章不存在' })
}
articles.splice(index, 1) // 从数组中移除
res.json({ message: '删除成功' })
})
app.listen(PORT, () => {
console.log(`博客 CRUD API 已启动 http://localhost:${PORT}`)
})
运行:
node 2_blog_crud.js
测试步骤:
# 1. 查看所有文章
curl http://localhost:3000/articles
# 2. 创建新文章
curl -X POST http://localhost:3000/articles \
-H "Content-Type: application/json" \
-d '{"title":"第三篇","content":"新写的内容"}'
# 3. 查看单篇(假设上面返回 id=3)
curl http://localhost:3000/articles/3
# 4. 修改文章
curl -X PUT http://localhost:3000/articles/3 \
-H "Content-Type: application/json" \
-d '{"title":"第三篇(已修改)"}'
# 5. 删除文章
curl -X DELETE http://localhost:3000/articles/3
预期输出(创建新文章后):
{"message":"创建成功","article":{"id":3,"title":"第三篇","content":"新写的内容"}}
一句话解释:CRUD 就是 Create(创建)、Read(读取)、Update(更新)、Delete(删除),这5个路由覆盖了所有基本操作。
📦 项目 3(15分钟):加上"门卫"——JWT 鉴权简单实现
现在谁都能操作文章,太不安全了。加上一个简单的 token 验证,只有带"通行证"的人才能发文章。
完整代码:
// 3_blog_with_auth.js
const express = require('express')
const app = express()
const PORT = 3000
app.use(express.json())
// 模拟数据库
let articles = [
{ id: 1, title: '第一篇文章', content: '这是内容' },
{ id: 2, title: '第二篇文章', content: '另一篇内容' }
]
let nextId = 3
// 模拟用户数据(真实项目存在数据库里)
const users = [
{ id: 1, username: 'admin', password: '123456' } // 密码真实项目要加密!
]
// 简单 token 表(真实项目用 JWT)
const tokens = []
// ========== 中间件:验证 token ==========
const authenticate = (req, res, next) => {
const token = req.headers['authorization']
if (!token || !tokens.includes(token)) {
return res.status(401).json({ error: '未授权,请先登录' })
}
next() // 验证通过,继续
}
// ========== 公开路由 ==========
// 登录
app.post('/login', (req, res) => {
const { username, password } = req.body
const user = users.find(u => u.username === username && u.password === password)
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' })
}
// 生成简单 token(真实项目用 JWT)
const token = `token_${user.id}_${Date.now()}`
tokens.push(token)
res.json({ message: '登录成功', token })
})
// 获取文章(公开)
app.get('/articles', (req, res) => {
res.json(articles)
})
// ========== 需要登录的路由 ==========
// 创建文章(需要 token)
app.post('/articles', authenticate, (req, res) => {
const { title, content } = req.body
if (!title || !content) {
return res.status(400).json({ error: '标题和内容不能为空' })
}
const newArticle = { id: nextId++, title, content }
articles.push(newArticle)
res.status(201).json({ message: '创建成功', article: newArticle })
})
// 删除文章(需要 token)
app.delete('/articles/:id', authenticate, (req, res) => {
const id = parseInt(req.params.id)
const index = articles.findIndex(a => a.id === id)
if (index === -1) {
return res.status(404).json({ error: '文章不存在' })
}
articles.splice(index, 1)
res.json({ message: '删除成功' })
})
app.listen(PORT, () => {
console.log(`带鉴权的博客 API 已启动 http://localhost:${PORT}`)
})
运行:
node 3_blog_with_auth.js
测试流程:
# 1. 不带 token 直接创建 → 失败
curl -X POST http://localhost:3000/articles \
-H "Content-Type: application/json" \
-d '{"title":"测试","content":"内容"}'
# 返回:{"error":"未授权,请先登录"}
# 2. 登录获取 token
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 返回:{"message":"登录成功","token":"token_1_1234567890"}
# 3. 带 token 创建文章 → 成功
curl -X POST http://localhost:3000/articles \
-H "Content-Type: application/json" \
-H "Authorization: token_1_1234567890" \
-d '{"title":"认证后才敢发","content":"这篇文章安全多了"}'
预期输出:
{"message":"创建成功","article":{"id":3,"title":"认证后才敢发","content":"这篇文章安全多了"}}
一句话解释:authenticate 中间件就像门卫,不带工牌(token)的访客直接拦下,只有验证通过才放行。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:忘记 next() 导致请求卡死 ❌ → ✅
// ❌ 错误:没有调用 next(),请求永远等不到响应
app.use((req, res, next) => {
console.log('收到了请求')
// 忘了 next()
})
// ✅ 正确:记得放行
app.use((req, res, next) => {
console.log('收到了请求')
next()
})
坑 2:修改了请求体但没解析 ❌ → ✅
// ❌ 错误:req.body 是 undefined
app.post('/articles', (req, res) => {
console.log(req.body.title) // undefined!
})
// ✅ 正确:先加 express.json() 中间件
app.use(express.json())
app.post('/articles', (req, res) => {
console.log(req.body.title) // 正常拿到
})
坑 3:URL 参数是字符串,不是数字 ❌ → ✅
// ❌ 错误:比较类型不匹配
app.get('/articles/:id', (req, res) => {
const article = articles.find(a => a.id === req.params.id)
// req.params.id 是 "1"(字符串),但 articles 里是 1(数字)
})
// 找不到!
// ✅ 正确:转换类型
app.get('/articles/:id', (req, res) => {
const id = parseInt(req.params.id) // 转成数字
const article = articles.find(a => a.id === id)
})
坑 4:忘记 return 导致"双响应"错误 ❌ → ✅
// ❌ 错误:两次发送响应会崩溃
app.get('/articles/:id', (req, res) => {
if (!article) {
res.status(404).json({ error: '不存在' }) // 没 return
}
res.json(article) // 又发了一次!
})
// ✅ 正确:及时 return
app.get('/articles/:id', (req, res) => {
if (!article) {
return res.status(404).json({ error: '不存在' }) // 加上 return
}
res.json(article)
})
坑 5:生产环境用内存存储数据 ❌ → ✅
// ❌ 错误:重启服务器数据全丢
let articles = [{ id: 1, title: '文章' }]
// 服务器一关,全没了
// ✅ 正确:下一章我们用 MySQL 数据库持久化存储
// 先预告一下:
// const mysql = require('mysql2')
// const db = mysql.createConnection({ host: 'localhost', user: 'root', password: '密码', database: 'blog' })
性能小贴士:生产环境加压缩和日志
const compression = require('compression')
app.use(compression()) // 自动压缩响应,传输更快
// 日志用 morgan
const morgan = require('morgan')
app.use(morgan('dev')) // 打印请求日志
调试技巧:用 console.log 定位问题
// 在关键位置打印变量
app.post('/articles', (req, res) => {
console.log('收到请求体:', req.body) // 看看收到了什么
console.log('当前所有文章:', articles) // 看看数据库状态
// 如果还是找不到问题,加更多打印
const { title, content } = req.body
console.log('解析后:', { title, content })
// ...
})
✏️ 练习题 + 作业题
练习题(5道,10分钟内完成)
练习 1(2分钟):改端口号
- 输入:在项目1代码中把端口从 3000 改成 8080
- 预期输出:启动时显示 http://localhost:8080
- 提示:只改 PORT 变量的值
练习 2(2分钟):加一个"获取文章数量"的路由
- 输入:在项目2代码中添加一个新路由,返回 { count: 2 }
- 预期输出:访问 GET /articles/count 返回 {"count":2}(假设有2篇文章)
- 提示:复用 articles.length
练习 3(3分钟):给"修改文章"加上作者验证
- 输入:在项目3的基础上,让删除也需要登录
- 预期输出:不带 token 调用 DELETE 返回 401 错误
- 提示:给 app.delete('/articles/:id', ...) 加上 authenticate 中间件
练习 4(3分钟):实现"按标题搜索"功能
- 输入:访问 GET /articles/search?keyword=第一
- 预期输出:返回标题包含"第一"的文章列表
- 提示:用 articles.filter() + includes()
练习 5(挑战题,5分钟):分析这个报错
- 输入:某同学运行代码后,所有 POST 请求都报 Cannot read property 'title' of undefined
- 预期输出:说出原因并修复
- 提示:检查请求头(Content-Type)和 body 解析
作业题(30分钟-2小时)
作业:做一个「博客评论系统 API」
- 需求描述:给博客文章加评论功能,支持查看评论、添加评论、删除评论
- 功能点:
1.GET /articles/:id/comments- 获取某篇文章的所有评论
2.POST /articles/:id/comments- 给某篇文章添加评论(需登录)
3.DELETE /comments/:id- 删除某条评论(需登录,是评论作者才能删) - 数据结构:
comments = [
{ id: 1, articleId: 1, author: '张三', content: '写得好!' },
{ id: 2, articleId: 1, author: '李四', content: '学习了' }
]
- 加分项:
1. 加一个GET /articles/:id/comments/count返回评论数
2. 评论支持回复(加一个parentId字段表示回复哪条评论) - 验收标准:能跑起来 + 3个基本功能都通 + 有简单注释
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 件事:
- REST API 规范:用 GET/POST/PUT/DELETE 对应查/增/改/删
- Express 路由:用
:id接收 URL 参数,用中间件做验证 - 简单鉴权:用 token 保护需要登录的操作
延伸学习资源:
- 📖 Express 官方文档:https://expressjs.com/zh-cn/starter/basic-routing.html
- 📖 《深入浅出 Node.js》:朴灵著,对异步和事件循环讲得很透
- 🎬 B站搜索「Node.js 实战」:找评分高的跟着敲
互动钩子:
你在做博客系统的时候,有没有被"增删改查"绕晕过?或者被 token 验证坑过?评论区聊聊,老粉优先回复!
下章预告:
现在文章存在内存里,服务器一重启全没了——下一章我们接上 MySQL 数据库,让数据真正持久保存!🚀

评论(0)