Node.js 博客 API 实战
这篇把前面所有知识串起来,从零搭建一个完整的博客系统后端 API。你会看到真实项目怎么分层组织代码,注册登录、文章、评论、权限控制怎么组合在一起,最后用 Swagger 生成接口文档。
功能规划
先定目标,一个博客后端需要这些接口
| 模块 | 功能 | 接口 |
|---|---|---|
| 用户 | 注册、登录 | POST /api/auth/register、POST /api/auth/login |
| 用户 | 查看自己的信息 | GET /api/users/me |
| 文章 | 增删改查 | POST /api/posts、GET /api/posts、GET /api/posts/:id、PUT /api/posts/:id、DELETE /api/posts/:id |
| 评论 | 发表、列表 | POST /api/posts/:id/comments、GET /api/posts/:id/comments |
权限规则,文章和评论只有登录用户能创建,文章只有作者本人能修改和删除。这个项目复用了前面的全部成果,Express 入门见 Node.js Express 入门,数据库和关联查询见 Node.js Prisma ORM 和 Node.js 数据库设计与关联,认证见 Node.js JWT 身份认证。
项目结构
分层架构是真实项目的标准做法,每层只干一件事
blog-api/
├── prisma/
│ └── schema.prisma
├── src/
│ ├── app.js 入口,装配中间件和路由
│ ├── server.js 启动服务器
│ ├── routes/ 路由定义,只做 URL 分发
│ │ ├── auth.routes.js
│ │ ├── user.routes.js
│ │ ├── post.routes.js
│ │ └── comment.routes.js
│ ├── controllers/ 控制器,处理请求和响应
│ │ ├── auth.controller.js
│ │ ├── user.controller.js
│ │ ├── post.controller.js
│ │ └── comment.controller.js
│ ├── services/ 业务逻辑,复杂的规则放这里
│ ├── middlewares/ 中间件
│ │ ├── auth.middleware.js
│ │ └── error.middleware.js
│ └── utils/
│ └── response.js 统一响应格式
├── .env
└── package.json
依赖清单,npm install 一次装齐
npm install express @prisma/client bcryptjs jsonwebtoken cors
npm install -D prisma
生产依赖和开发依赖的区别见 Node.js npm 入门。
数据模型
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
username String @unique
password String
posts Post[]
comments Comment[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Comment[]
createdAt DateTime @default(now())
}
model Comment {
id Int @id @default(autoincrement())
content String
post Post @relation(fields: [postId], references: [id])
postId Int
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
模型字段的含义和关联关系写法,回顾 Node.js 数据库设计与关联。执行迁移建表
npx prisma migrate dev --name init
统一响应与错误处理
所有接口返回固定结构 { code, data, message },客户端拿到 code 就知道成功失败,设计规范见 Node.js RESTful API 设计。
// src/utils/response.js
function success(res, data, message = 'ok', code = 0) {
res.json({ code, data, message })
}
function fail(res, status, message, code = status) {
res.status(status).json({ code, data: null, message })
}
module.exports = { success, fail }
全局错误处理中间件,在 app.js 里注册到所有路由之后
// src/middlewares/error.middleware.js
function notFound(req, res) {
res.status(404).json({ code: 404, data: null, message: '接口不存在' })
}
function errorHandler(err, req, res, next) {
console.error(err)
res.status(500).json({ code: 500, data: null, message: '服务器内部错误' })
}
module.exports = { notFound, errorHandler }
错误处理中间件的四个参数和注册顺序,回顾 Node.js Express 中间件。
认证模块
注册时用 bcryptjs 哈希密码,绝不能存明文。登录成功签发 JWT,有效期 7 天。
// src/controllers/auth.controller.js
const { PrismaClient } = require('@prisma/client')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { success, fail } = require('../utils/response')
const prisma = new PrismaClient()
const JWT_SECRET = process.env.JWT_SECRET
async function register(req, res) {
const { username, password } = req.body
if (!username || !password) {
return fail(res, 400, '用户名和密码不能为空')
}
const exists = await prisma.user.findUnique({ where: { username } })
if (exists) {
return fail(res, 400, '用户名已存在')
}
const hash = await bcrypt.hash(password, 10)
const user = await prisma.user.create({
data: { username, password: hash }
})
success(res, { id: user.id, username: user.username }, '注册成功')
}
async function login(req, res) {
const { username, password } = req.body
const user = await prisma.user.findUnique({ where: { username } })
if (!user) {
return fail(res, 400, '用户名或密码错误')
}
const ok = await bcrypt.compare(password, user.password)
if (!ok) {
return fail(res, 400, '用户名或密码错误')
}
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' })
success(res, { token }, '登录成功')
}
module.exports = { register, login }
密码哈希和 JWT 签发的原理,回顾 Node.js JWT 身份认证。
鉴权中间件,拦截所有需要登录的接口
// src/middlewares/auth.middleware.js
const jwt = require('jsonwebtoken')
function authMiddleware(req, res, next) {
const header = req.headers.authorization
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ code: 401, data: null, message: '请先登录' })
}
const token = header.slice(7)
try {
const payload = jwt.verify(token, process.env.JWT_SECRET)
req.userId = payload.userId
next()
} catch (err) {
res.status(401).json({ code: 401, data: null, message: '登录已过期' })
}
}
module.exports = authMiddleware
客户端请求时在请求头带上 Authorization: Bearer <token>,后端就能识别是谁。
文章模块
文章列表要带出作者用户名,用 include 预加载关联数据,同时实现分页。
// src/controllers/post.controller.js
const { PrismaClient } = require('@prisma/client')
const { success, fail } = require('../utils/response')
const prisma = new PrismaClient()
async function list(req, res) {
const page = Number(req.query.page) || 1
const pageSize = Number(req.query.pageSize) || 10
const posts = await prisma.post.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
include: { author: { select: { id: true, username: true } } }
})
const total = await prisma.post.count()
success(res, { list: posts, total })
}
async function create(req, res) {
const { title, content } = req.body
if (!title || !content) {
return fail(res, 400, '标题和内容不能为空')
}
const post = await prisma.post.create({
data: { title, content, authorId: req.userId }
})
success(res, post, '发布成功')
}
async function update(req, res) {
const id = Number(req.params.id)
const post = await prisma.post.findUnique({ where: { id } })
if (!post) {
return fail(res, 404, '文章不存在')
}
if (post.authorId !== req.userId) {
return fail(res, 403, '只能修改自己的文章')
}
const updated = await prisma.post.update({
where: { id },
data: { title: req.body.title, content: req.body.content }
})
success(res, updated, '更新成功')
}
async function remove(req, res) {
const id = Number(req.params.id)
const post = await prisma.post.findUnique({ where: { id } })
if (!post) {
return fail(res, 404, '文章不存在')
}
if (post.authorId !== req.userId) {
return fail(res, 403, '只能删除自己的文章')
}
await prisma.post.delete({ where: { id } })
success(res, null, '删除成功')
}
module.exports = { list, create, update, remove }
权限判断的逻辑在 update 和 remove 里,比较 post.authorId 和 req.userId,不相等返回 403,这是接口级权限控制的常见写法。
路由与入口
路由文件只做分发,业务全部在控制器里
// src/routes/post.routes.js
const express = require('express')
const authMiddleware = require('../middlewares/auth.middleware')
const postController = require('../controllers/post.controller')
const router = express.Router()
router.get('/', postController.list)
router.post('/', authMiddleware, postController.create)
router.put('/:id', authMiddleware, postController.update)
router.delete('/:id', authMiddleware, postController.remove)
module.exports = router
评论模块结构相同,创建评论时从 URL 取 postId,从中间件取 userId,这里不再重复贴代码。app.js 把所有模块装到一起
// src/app.js
const express = require('express')
const cors = require('cors')
const authRoutes = require('./routes/auth.routes')
const userRoutes = require('./routes/user.routes')
const postRoutes = require('./routes/post.routes')
const commentRoutes = require('./routes/comment.routes')
const { notFound, errorHandler } = require('./middlewares/error.middleware')
const app = express()
app.use(cors())
app.use(express.json())
app.use('/api/auth', authRoutes)
app.use('/api/users', userRoutes)
app.use('/api/posts', postRoutes)
app.use('/api/posts/:id/comments', commentRoutes)
app.use(notFound)
app.use(errorHandler)
module.exports = app
// src/server.js
const app = require('./app')
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`博客 API 已启动,端口 ${PORT}`)
})
API 文档
Swagger 能把接口文档挂在网页上,前端照着文档对接。安装后写一份 OpenAPI 描述文件
npm install swagger-jsdoc swagger-ui-express
// src/swagger.js
const swaggerJsdoc = require('swagger-jsdoc')
const swaggerUi = require('swagger-ui-express')
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '博客 API',
version: '1.0.0'
}
},
apis: ['./src/routes/*.js']
}
const specs = swaggerJsdoc(options)
module.exports = { swaggerUi, specs }
在 app.js 里挂载,启动后访问 http://localhost:3000/api-docs 就能看到接口文档
const { swaggerUi, specs } = require('./swagger')
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs))
实践任务
按上面的结构从零搭建完整博客 API,验收标准
- 注册两个用户,都能正常登录拿到 token
- 用户 A 发布文章,用户 B 尝试修改 A 的文章,接口返回 403
- 文章列表接口返回作者用户名,评论接口正常创建和查询
- 打开 /api-docs 能看到全部接口文档
- 未登录访问发布接口返回 401
写完检查一遍路由顺序和鉴权中间件有没有遗漏,这些是最容易出错的地方。下一篇把项目部署到服务器,让它在公网跑起来,见 Node.js 部署与日志。