欢迎光临
我们一直在努力

Node.js + Express 实战,从零搭建用户认证与文章管理 API

学完理论,我们来做一个真正的项目。

一个带 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>)

完整项目已包含以上所有代码,复制即可使用。

赞(0)
未经允许不得转载:171主机测评 » Node.js + Express 实战,从零搭建用户认证与文章管理 API
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址