第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', ...) // 删除某篇文章

配图1 - 配图1

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 对象。

配图2 - 配图2


🔥 实战 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 件事:

  1. REST API 规范:用 GET/POST/PUT/DELETE 对应查/增/改/删
  2. Express 路由:用 :id 接收 URL 参数,用中间件做验证
  3. 简单鉴权:用 token 保护需要登录的操作

延伸学习资源:

  • 📖 Express 官方文档:https://expressjs.com/zh-cn/starter/basic-routing.html
  • 📖 《深入浅出 Node.js》:朴灵著,对异步和事件循环讲得很透
  • 🎬 B站搜索「Node.js 实战」:找评分高的跟着敲

互动钩子:

你在做博客系统的时候,有没有被"增删改查"绕晕过?或者被 token 验证坑过?评论区聊聊,老粉优先回复!


下章预告:

现在文章存在内存里,服务器一重启全没了——下一章我们接上 MySQL 数据库,让数据真正持久保存!🚀

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。