学完理论,我们来做一个真正的项目。
一个带 JWT 认证、MongoDB 存储的用户系统 + 文章管理 API。
1. 项目介绍
我们要做一个 用户认证 + 文章管理 的后端 API,包含以下功能:
用户注册、登录、获取个人信息
文章的增删改查
只有登录用户才能创建/修改/删除文章
每篇文章属于一个用户,用户只能操作自己的文章
技术栈:
- Node.js + Express
- MongoDB + Mongoose
- JWT 身份认证
- bcryptjs 密码加密
- dotenv 环境变量
最终 API 列表:
| POST | /api/auth/register | 用户注册 | 否 |
| POST | /api/auth/login | 用户登录 | 否 |
| GET | /api/auth/me | 获取当前用户信息 | 是 |
| GET | /api/posts | 获取所有文章 | 否 |
| GET | /api/posts/:id | 获取单篇文章 | 否 |
| POST | /api/posts | 发布文章 | 是 |
| PUT | /api/posts/:id | 更新自己的文章 | 是 |
| DELETE | /api/posts/:id | 删除自己的文章 | 是 |
2. 项目初始化
2.1 创建项目
mkdir node-api-demo
cd node-api-demo
npm init -y
2.2 安装依赖
npm install express mongoose jsonwebtoken bcryptjs dotenv cors
npm install -D nodemon
2.3 目录结构
node-api-demo/
├── .env
├── .gitignore
├── package.json
├── server.js
├── models/
│ ├── User.js
│ └── Post.js
├── middleware/
│ └── auth.js
├── routes/
│ ├── auth.js
│ └── posts.js
└── controllers/
├── authController.js
└── postsController.js
3. 环境变量配置
创建 .env 文件:
PORT=5000
MONGODB_URI=mongodb://localhost:27017/node_api_demo
JWT_SECRET=your_super_secret_key_change_me
4. 连接数据库
创建 server.js 入口文件:
// server.js
require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const cors = require('cors')
const app = express()
// 中间件
app.use(cors())
app.use(express.json())
// 连接数据库
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('√ MongoDB connected'))
.catch(err => console.error('X MongoDB connection error:', err))
// 路由
app.use('/api/auth', require('./routes/auth'))
app.use('/api/posts', require('./routes/posts'))
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).json({ message: '服务器内部错误' })
})
// 启动服务器
const PORT = process.env.PORT || 5000
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
5. 用户模型(User Model)
创建 models/User.js:
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
},
createdAt: {
type: Date,
default: Date.now
}
})
// 保存前加密密码
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next()
const salt = await bcrypt.genSalt(10)
this.password = await bcrypt.hash(this.password, salt)
next()
})
// 验证密码的方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password)
}
module.exports = mongoose.model('User', userSchema)
6. 文章模型(Post Model)
创建 models/Post.js:
const mongoose = require('mongoose')
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
maxlength: 100
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
})
postSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
module.exports = mongoose.model('Post', postSchema)
7. JWT 认证中间件
创建 middleware/auth.js:
const jwt = require('jsonwebtoken')
const User = require('../models/User')
module.exports = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ message: '未提供认证令牌' })
}
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findById(decoded.userId).select('-password')
if (!user) {
return res.status(401).json({ message: '用户不存在' })
}
req.user = user
next()
} catch (error) {
console.error(error)
res.status(401).json({ message: '认证失败,请重新登录' })
}
}
8. 控制器(业务逻辑)
8.1 认证控制器
创建 controllers/authController.js:
const jwt = require('jsonwebtoken')
const User = require('../models/User')
// 生成 JWT
const generateToken = (userId) => {
return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '7d' })
}
// 用户注册
exports.register = async (req, res) => {
try {
const { username, email, password } = req.body
// 检查用户是否已存在
const existingUser = await User.findOne({ $or: [{ email }, { username }] })
if (existingUser) {
return res.status(400).json({ message: '用户名或邮箱已被注册' })
}
// 创建新用户
const user = new User({ username, email, password })
await user.save()
const token = generateToken(user._id)
res.status(201).json({
message: '注册成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
})
} catch (error) {
console.error(error)
res.status(500).json({ message: '服务器错误' })
}
}
// 用户登录
exports.login = async (req, res) => {
try {
const { email, password } = req.body
const user = await User.findOne({ email })
if (!user) {
return res.status(401).json({ message: '邮箱或密码错误' })
}
const isMatch = await user.comparePassword(password)
if (!isMatch) {
return res.status(401).json({ message: '邮箱或密码错误' })
}
const token = generateToken(user._id)
res.json({
message: '登录成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
})
} catch (error) {
console.error(error)
res.status(500).json({ message: '服务器错误' })
}
}
// 获取当前用户信息
exports.getMe = async (req, res) => {
res.json({
user: {
id: req.user._id,
username: req.user.username,
email: req.user.email
}
})
}
8.2 文章控制器
创建 controllers/postsController.js:
const Post = require('../models/Post')
// 获取所有文章(包含作者信息)
exports.getAllPosts = async (req, res) => {
try {
const posts = await Post.find()
.populate('author', 'username email')
.sort({ createdAt: -1 })
res.json(posts)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 获取单篇文章
exports.getPostById = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'username email')
if (!post) {
return res.status(404).json({ message: '文章不存在' })
}
res.json(post)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 创建文章
exports.createPost = async (req, res) => {
try {
const { title, content } = req.body
const post = new Post({
title,
content,
author: req.user._id
})
await post.save()
await post.populate('author', 'username email')
res.status(201).json(post)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 更新文章
exports.updatePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
if (!post) {
return res.status(404).json({ message: '文章不存在' })
}
// 检查权限
if (post.author.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: '无权限修改此文章' })
}
post.title = req.body.title || post.title
post.content = req.body.content || post.content
await post.save()
await post.populate('author', 'username email')
res.json(post)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 删除文章
exports.deletePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
if (!post) {
return res.status(404).json({ message: '文章不存在' })
}
if (post.author.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: '无权限删除此文章' })
}
await post.deleteOne()
res.json({ message: '文章已删除' })
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
9. 路由定义
9.1 认证路由
创建 routes/auth.js:
const express = require('express')
const router = express.Router()
const authController = require('../controllers/authController')
const authMiddleware = require('../middleware/auth')
router.post('/register', authController.register)
router.post('/login', authController.login)
router.get('/me', authMiddleware, authController.getMe)
module.exports = router
9.2 文章路由
创建 routes/posts.js:
const express = require('express')
const router = express.Router()
const postsController = require('../controllers/postsController')
const authMiddleware = require('../middleware/auth')
// 公开路由
router.get('/', postsController.getAllPosts)
router.get('/:id', postsController.getPostById)
// 需要登录的路由
router.post('/', authMiddleware, postsController.createPost)
router.put('/:id', authMiddleware, postsController.updatePost)
router.delete('/:id', authMiddleware, postsController.deletePost)
module.exports = router
10. 测试 API
10.1 使用 cURL 或 Postman
1. 注册用户
POST http://localhost:5000/api/auth/register
Content-Type: application/json
{
"username": "john_doe",
"email": "john@example.com",
"password": "123456"
}
2. 登录
POST http://localhost:5000/api/auth/login
Content-Type: application/json
{
"email": "john@example.com",
"password": "123456"
}
响应会包含 token,后续请求需要将其放入 Authorization 头。
3. 创建文章(需要 token)
POST http://localhost:5000/api/posts
Authorization: Bearer <your-token>
Content-Type: application/json
{
"title": "我的第一篇博客",
"content": "这是使用 Node.js + Express 写的 API"
}
4. 获取所有文章
GET http://localhost:5000/api/posts
10.2 使用 Postman 集合
建议在 Postman 中创建环境变量 token,然后在登录请求的 Tests 中自动设置:
const jsonData = pm.response.json();
pm.environment.set('token', jsonData.token);
11. 部署到生产环境
参考上一章的部署指南(Railway / Render)。注意需要在生产环境中配置环境变量 MONGODB_URI 和 JWT_SECRET。
12. 完整项目代码
1. .env
PORT=5000
MONGODB_URI=mongodb://localhost:27017/node_api_demo
JWT_SECRET=your_super_secret_key_change_me
2. .gitignore
node_modules/
.env
.DS_Store
3. package.json
{
"name": "node-api-demo",
"version": "1.0.0",
"description": "用户认证 + 文章管理 API",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
}
4. server.js
require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const cors = require('cors')
const app = express()
// 中间件
app.use(cors())
app.use(express.json())
// 连接数据库
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('√ MongoDB connected'))
.catch(err => console.error('X MongoDB connection error:', err))
// 路由
app.use('/api/auth', require('./routes/auth'))
app.use('/api/posts', require('./routes/posts'))
// 404 处理
app.use((req, res) => {
res.status(404).json({ message: '路由不存在' })
})
// 全局错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).json({ message: '服务器内部错误' })
})
// 启动服务器
const PORT = process.env.PORT || 5000
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
5. models/User.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
},
createdAt: {
type: Date,
default: Date.now
}
})
// 保存前加密密码
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next()
const salt = await bcrypt.genSalt(10)
this.password = await bcrypt.hash(this.password, salt)
next()
})
// 验证密码的方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password)
}
module.exports = mongoose.model('User', userSchema)
6. models/Post.js
const mongoose = require('mongoose')
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
maxlength: 100
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
})
postSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
module.exports = mongoose.model('Post', postSchema)
7. middleware/auth.js
const jwt = require('jsonwebtoken')
const User = require('../models/User')
module.exports = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ message: '未提供认证令牌' })
}
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findById(decoded.userId).select('-password')
if (!user) {
return res.status(401).json({ message: '用户不存在' })
}
req.user = user
next()
} catch (error) {
console.error(error)
res.status(401).json({ message: '认证失败,请重新登录' })
}
}
8. controllers/authController.js
const jwt = require('jsonwebtoken')
const User = require('../models/User')
// 生成 JWT
const generateToken = (userId) => {
return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '7d' })
}
// 用户注册
exports.register = async (req, res) => {
try {
const { username, email, password } = req.body
// 检查用户是否已存在
const existingUser = await User.findOne({ $or: [{ email }, { username }] })
if (existingUser) {
return res.status(400).json({ message: '用户名或邮箱已被注册' })
}
// 创建新用户
const user = new User({ username, email, password })
await user.save()
const token = generateToken(user._id)
res.status(201).json({
message: '注册成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
})
} catch (error) {
console.error(error)
res.status(500).json({ message: '服务器错误' })
}
}
// 用户登录
exports.login = async (req, res) => {
try {
const { email, password } = req.body
const user = await User.findOne({ email })
if (!user) {
return res.status(401).json({ message: '邮箱或密码错误' })
}
const isMatch = await user.comparePassword(password)
if (!isMatch) {
return res.status(401).json({ message: '邮箱或密码错误' })
}
const token = generateToken(user._id)
res.json({
message: '登录成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
})
} catch (error) {
console.error(error)
res.status(500).json({ message: '服务器错误' })
}
}
// 获取当前用户信息
exports.getMe = async (req, res) => {
res.json({
user: {
id: req.user._id,
username: req.user.username,
email: req.user.email
}
})
}
9. controllers/postsController.js
const Post = require('../models/Post')
// 获取所有文章(包含作者信息)
exports.getAllPosts = async (req, res) => {
try {
const posts = await Post.find()
.populate('author', 'username email')
.sort({ createdAt: -1 })
res.json(posts)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 获取单篇文章
exports.getPostById = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'username email')
if (!post) {
return res.status(404).json({ message: '文章不存在' })
}
res.json(post)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 创建文章
exports.createPost = async (req, res) => {
try {
const { title, content } = req.body
const post = new Post({
title,
content,
author: req.user._id
})
await post.save()
await post.populate('author', 'username email')
res.status(201).json(post)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 更新文章
exports.updatePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
if (!post) {
return res.status(404).json({ message: '文章不存在' })
}
// 检查权限
if (post.author.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: '无权限修改此文章' })
}
post.title = req.body.title || post.title
post.content = req.body.content || post.content
await post.save()
await post.populate('author', 'username email')
res.json(post)
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
// 删除文章
exports.deletePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
if (!post) {
return res.status(404).json({ message: '文章不存在' })
}
if (post.author.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: '无权限删除此文章' })
}
await post.deleteOne()
res.json({ message: '文章已删除' })
} catch (error) {
res.status(500).json({ message: '服务器错误' })
}
}
10. routes/auth.js
const express = require('express')
const router = express.Router()
const authController = require('../controllers/authController')
const authMiddleware = require('../middleware/auth')
router.post('/register', authController.register)
router.post('/login', authController.login)
router.get('/me', authMiddleware, authController.getMe)
module.exports = router
11. routes/posts.js
const express = require('express')
const router = express.Router()
const postsController = require('../controllers/postsController')
const authMiddleware = require('../middleware/auth')
// 公开路由
router.get('/', postsController.getAllPosts)
router.get('/:id', postsController.getPostById)
// 需要登录的路由
router.post('/', authMiddleware, postsController.createPost)
router.put('/:id', authMiddleware, postsController.updatePost)
router.delete('/:id', authMiddleware, postsController.deletePost)
module.exports = router
13. 运行指南
创建项目文件夹,将上述所有文件按目录结构放入。
在项目根目录打开终端,运行:
npm install
确保本地 MongoDB 服务已启动(或修改 .env 中的 MONGODB_URI 为 MongoDB Atlas 云数据库地址)。
启动服务:
npm run dev
使用 Postman / cURL 测试接口,示例:
- 注册:POST http://localhost:5000/api/auth/register
- 登录:POST http://localhost:5000/api/auth/login
- 创建文章:POST http://localhost:5000/api/posts(需在 Header 中携带 Authorization: Bearer <token>)
完整项目已包含以上所有代码,复制即可使用。


