项目初始化与目录结构
一个良好的项目结构是可维护性的基础。下面是一个经过实践验证的 Express.js 项目结构:
mkdir my-api && cd my-api
npm init -y
npm install express cors helmet morgan dotenv
npm install -D nodemon
my-api/
├── src/
│ ├── config/ # 配置文件
│ │ └── database.js
│ ├── controllers/ # 控制器(业务逻辑)
│ │ └── userController.js
│ ├── middleware/ # 中间件
│ │ ├── auth.js
│ │ ├── errorHandler.js
│ │ └── validate.js
│ ├── models/ # 数据模型
│ │ └── User.js
│ ├── routes/ # 路由定义
│ │ ├── index.js
│ │ └── userRoutes.js
│ ├── utils/ # 工具函数
│ │ └── ApiError.js
│ └── app.js # Express 应用入口
├── .env # 环境变量
├── .gitignore
└── package.json
Express 应用基础搭建
// src/app.js
const express = require('express')
const cors = require('cors')
const helmet = require('helmet')
const morgan = require('morgan')
const routes = require('./routes')
const errorHandler = require('./middleware/errorHandler')
const app = express()
// 安全中间件
app.use(helmet())
app.use(cors({ origin: process.env.CORS_ORIGIN || '*' }))
// 请求解析
app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true }))
// 日志
app.use(morgan('dev'))
// API 路由
app.use('/api', routes)
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() })
})
// 404 处理
app.use((req, res) => {
res.status(404).json({ error: '接口不存在' })
})
// 全局错误处理
app.use(errorHandler)
module.exports = app
路由设计 —— RESTful 规范
RESTful API 的核心是用 HTTP 方法表示操作,用 URL 表示资源:
| 方法 | 路径 | 说明 | 状态码 |
|---|---|---|---|
| GET | /api/users | 获取用户列表 | 200 |
| GET | /api/users/:id | 获取单个用户 | 200 / 404 |
| POST | /api/users | 创建用户 | 201 |
| PUT | /api/users/:id | 更新用户(全量) | 200 / 404 |
| PATCH | /api/users/:id | 更新用户(部分) | 200 / 404 |
| DELETE | /api/users/:id | 删除用户 | 204 / 404 |
// src/routes/userRoutes.js
const router = require('express').Router()
const ctrl = require('../controllers/userController')
const auth = require('../middleware/auth')
const validate = require('../middleware/validate')
const { createUserSchema, updateUserSchema } = require('../validations/user')
router.get('/', ctrl.list)
router.get('/:id', ctrl.getById)
router.post('/', validate(createUserSchema), ctrl.create)
router.put('/:id', auth, validate(updateUserSchema), ctrl.update)
router.delete('/:id', auth, ctrl.remove)
module.exports = router
// src/routes/index.js
const router = require('express').Router()
router.use('/users', require('./userRoutes'))
module.exports = router
控制器 —— 业务逻辑层
// src/controllers/userController.js
const User = require('../models/User')
const ApiError = require('../utils/ApiError')
exports.list = async (req, res, next) => {
try {
const { page = 1, limit = 20, keyword } = req.query
const filter = {}
if (keyword) {
filter.$or = [
{ name: new RegExp(keyword, 'i') },
{ email: new RegExp(keyword, 'i') }
]
}
const [users, total] = await Promise.all([
User.find(filter)
.skip((page - 1) * limit)
.limit(Number(limit))
.sort({ createdAt: -1 })
.select('-password'),
User.countDocuments(filter)
])
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total,
pages: Math.ceil(total / limit)
}
})
} catch (err) {
next(err)
}
}
exports.getById = async (req, res, next) => {
try {
const user = await User.findById(req.params.id).select('-password')
if (!user) throw new ApiError(404, '用户不存在')
res.json({ data: user })
} catch (err) {
next(err)
}
}
exports.create = async (req, res, next) => {
try {
const exists = await User.findOne({ email: req.body.email })
if (exists) throw new ApiError(409, '邮箱已注册')
const user = await User.create(req.body)
const { password, ...data } = user.toObject()
res.status(201).json({ data })
} catch (err) {
next(err)
}
}
exports.update = async (req, res, next) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true, runValidators: true }
).select('-password')
if (!user) throw new ApiError(404, '用户不存在')
res.json({ data: user })
} catch (err) {
next(err)
}
}
exports.remove = async (req, res, next) => {
try {
const user = await User.findByIdAndDelete(req.params.id)
if (!user) throw new ApiError(404, '用户不存在')
res.status(204).send()
} catch (err) {
next(err)
}
}
中间件系统
统一错误处理
// src/utils/ApiError.js
class ApiError extends Error {
constructor(statusCode, message) {
super(message)
this.statusCode = statusCode
this.isOperational = true
}
}
module.exports = ApiError
// src/middleware/errorHandler.js
module.exports = (err, req, res, next) => {
const statusCode = err.statusCode || 500
const message = err.isOperational ? err.message : '服务器内部错误'
// 开发环境打印完整错误
if (process.env.NODE_ENV !== 'production') {
console.error('Error:', err)
}
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
})
}
JWT 认证中间件
// src/middleware/auth.js
const jwt = require('jsonwebtoken')
module.exports = (req, res, next) => {
const header = req.headers.authorization
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供认证令牌' })
}
try {
const token = header.split(' ')[1]
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
next()
} catch (err) {
res.status(401).json({ error: '令牌无效或已过期' })
}
}
请求验证中间件
// src/middleware/validate.js
module.exports = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false })
if (error) {
const messages = error.details.map(d => d.message)
return res.status(400).json({ error: '参数验证失败', details: messages })
}
next()
}
// src/validations/user.js (使用 Joi)
const Joi = require('joi')
exports.createUserSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).max(100).required()
})
exports.updateUserSchema = Joi.object({
name: Joi.string().min(2).max(50),
email: Joi.string().email()
}).min(1) // 至少需要一个字段
数据库模型(Mongoose)
// src/models/User.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '用户名不能为空'],
trim: true,
minlength: 2,
maxlength: 50
},
email: {
type: String,
required: [true, '邮箱不能为空'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱地址']
},
password: {
type: String,
required: [true, '密码不能为空'],
minlength: 6,
select: false // 查询时默认不返回密码
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true // 自动添加 createdAt, updatedAt
})
// 保存前自动加密密码
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next()
this.password = await bcrypt.hash(this.password, 12)
next()
})
// 实例方法:验证密码
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password)
}
module.exports = mongoose.model('User', userSchema)
启动入口
// src/server.js
require('dotenv').config()
const mongoose = require('mongoose')
const app = require('./app')
const PORT = process.env.PORT || 3000
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/myapi'
mongoose.connect(MONGO_URI)
.then(() => {
console.log('数据库连接成功')
app.listen(PORT, () => {
console.log(`API 服务运行在 http://localhost:${PORT}`)
})
})
.catch(err => {
console.error('数据库连接失败:', err.message)
process.exit(1)
})
// package.json scripts
{
"scripts": {
"dev": "nodemon src/server.js",
"start": "node src/server.js"
}
}
使用 curl 测试 API
# 创建用户
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"博豪","email":"bohao@example.com","password":"123456"}'
# 获取用户列表(带分页和搜索)
curl "http://localhost:3000/api/users?page=1&limit=10&keyword=博豪"
# 获取单个用户
curl http://localhost:3000/api/users/60f7b3b5e6b3f32b8c9e4d71
# 更新用户(需要认证)
curl -X PUT http://localhost:3000/api/users/60f7b3b5e6b3f32b8c9e4d71 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name":"陈博豪"}'
# 删除用户
curl -X DELETE http://localhost:3000/api/users/60f7b3b5e6b3f32b8c9e4d71 \
-H "Authorization: Bearer <token>"
总结
本文从零搭建了一个完整的 Node.js RESTful API 服务,涵盖了路由设计、控制器、中间件、数据验证、JWT 认证、错误处理等核心环节。这些模式适用于绝大多数后端项目,可以直接作为新项目的起点。
下一步可以继续学习:Docker 容器化部署、自动化测试(Jest)、日志管理(Winston)、限流中间件、API 文档自动生成(Swagger)等进阶话题。