前言
在现代Web开发中,构建高性能、可扩展的RESTful API已成为后端开发的核心技能。Node.js凭借其非阻塞I/O模型和丰富的生态系统,在API开发领域占据重要地位。本文将系统性地介绍如何使用Node.js 18、Express框架和MongoDB数据库构建企业级RESTful API,涵盖从环境搭建到最佳实践的完整流程。
环境准备与项目初始化
Node.js 18环境配置
在开始之前,确保已安装Node.js 18或更高版本。可以通过以下命令检查:
node --version
npm --version
推荐使用nvm(Node Version Manager)来管理多个Node.js版本:
# 安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# 安装并切换到Node.js 18
nvm install 18
nvm use 18
项目初始化
创建项目目录并初始化:
mkdir node-api-project
cd node-api-project
npm init -y
安装核心依赖包:
# 核心依赖
npm install express mongoose dotenv cors helmet morgan
# 开发依赖
npm install --save-dev nodemon jest supertest eslint prettier
Express框架基础配置
基础服务器搭建
创建server.js文件,配置Express应用:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const mongoose = require('mongoose');
// 创建Express应用
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件配置
app.use(helmet()); // 安全头部设置
app.use(cors()); // 跨域支持
app.use(morgan('combined')); // 请求日志
app.use(express.json({ limit: '10mb' })); // JSON解析
app.use(express.urlencoded({ extended: true })); // URL编码解析
// 健康检查端点
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
service: 'API Server'
});
});
// 错误处理中间件(必须放在最后)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// 404处理
app.use('*', (req, res) => {
res.status(404).json({
error: 'Not Found',
message: 'The requested endpoint does not exist'
});
});
module.exports = app;
环境变量配置
创建.env文件:
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/api_db
JWT_SECRET=your-super-secret-jwt-key
API_VERSION=v1
MongoDB数据库设计与连接
数据库连接配置
创建config/database.js:
const mongoose = require('mongoose');
require('dotenv').config();
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error('Database connection error:', error);
process.exit(1);
}
};
// 连接事件监听
mongoose.connection.on('connected', () => {
console.log('Mongoose connected to database');
});
mongoose.connection.on('error', (err) => {
console.error('Mongoose connection error:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('Mongoose disconnected from database');
});
// 应用关闭时断开连接
process.on('SIGINT', async () => {
await mongoose.connection.close();
console.log('MongoDB connection closed due to application termination');
process.exit(0);
});
module.exports = connectDB;
数据模型设计
以用户管理为例,创建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,
maxlength: 30
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email']
},
password: {
type: String,
required: true,
minlength: 6
},
firstName: {
type: String,
required: true,
trim: true,
maxlength: 50
},
lastName: {
type: String,
required: true,
trim: true,
maxlength: 50
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
},
lastLogin: {
type: Date
}
}, {
timestamps: true, // 自动添加createdAt和updatedAt字段
toJSON: {
transform: function(doc, ret) {
delete ret.password;
delete ret.__v;
return ret;
}
}
});
// 密码加密中间件
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// 密码验证方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
RESTful API路由设计
基础路由结构
创建routes/users.js:
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authMiddleware = require('../middleware/auth');
// 公开端点 - 用户注册和登录
router.post('/register', userController.register);
router.post('/login', userController.login);
// 受保护的路由 - 需要认证
router.use(authMiddleware); // 应用认证中间件
// 用户管理相关路由
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.put('/:id', userController.updateUser);
router.delete('/:id', userController.deleteUser);
module.exports = router;
控制器实现
创建controllers/userController.js:
const User = require('../models/User');
const { validationResult } = require('express-validator');
const jwt = require('jsonwebtoken');
class UserController {
// 用户注册
async register(req, res) {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
const { username, email, password, firstName, lastName } = req.body;
// 检查用户是否已存在
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(409).json({
success: false,
message: 'User already exists'
});
}
// 创建新用户
const user = new User({
username,
email,
password,
firstName,
lastName
});
await user.save();
// 生成JWT令牌
const token = jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role
},
token
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}
// 用户登录
async login(req, res) {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
const { email, password } = req.body;
// 查找用户
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// 验证密码
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// 更新最后登录时间
user.lastLogin = new Date();
await user.save();
// 生成JWT令牌
const token = jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
success: true,
message: 'Login successful',
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role
},
token
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}
// 获取所有用户
async getAllUsers(req, res) {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User.find({ isActive: true })
.select('-password')
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
const total = await User.countDocuments({ isActive: true });
const totalPages = Math.ceil(total / limit);
res.json({
success: true,
data: {
users,
pagination: {
currentPage: page,
totalPages,
total,
limit
}
}
});
} catch (error) {
console.error('Get all users error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}
// 根据ID获取用户
async getUserById(req, res) {
try {
const user = await User.findById(req.params.id)
.select('-password');
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
console.error('Get user by ID error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}
// 更新用户
async updateUser(req, res) {
try {
const { username, email, firstName, lastName } = req.body;
const updateData = {
username,
email,
firstName,
lastName
};
const user = await User.findByIdAndUpdate(
req.params.id,
updateData,
{ new: true, runValidators: true }
).select('-password');
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
message: 'User updated successfully',
data: user
});
} catch (error) {
console.error('Update user error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}
// 删除用户
async deleteUser(req, res) {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
{ isActive: false },
{ new: true }
).select('-password');
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
message: 'User deleted successfully',
data: user
});
} catch (error) {
console.error('Delete user error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}
}
module.exports = new UserController();
中间件系统设计
认证中间件
创建middleware/auth.js:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
// 从请求头获取token
const authHeader = req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Access denied. No token provided.'
});
}
const token = authHeader.substring(7); // 移除 "Bearer " 前缀
// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 查找用户
const user = await User.findById(decoded.userId);
if (!user || !user.isActive) {
return res.status(401).json({
success: false,
message: 'Invalid token or user not found'
});
}
// 将用户信息附加到请求对象
req.user = {
id: user._id,
username: user.username,
email: user.email,
role: user.role
};
next();
} catch (error) {
console.error('Auth middleware error:', error);
res.status(401).json({
success: false,
message: 'Invalid token'
});
}
};
// 角色权限中间件
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Access denied. Insufficient permissions.'
});
}
next();
};
};
module.exports = { auth, authorize };
数据验证中间件
创建middleware/validation.js:
const { body, validationResult } = require('express-validator');
// 用户注册验证规则
const validateUserRegistration = [
body('username')
.isLength({ min: 3, max: 30 })
.withMessage('Username must be between 3 and 30 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Please provide a valid email address'),
body('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters long')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Password must contain at least one uppercase letter, one lowercase letter, and one number'),
body('firstName')
.isLength({ min: 1, max: 50 })
.withMessage('First name must be between 1 and 50 characters')
.matches(/^[a-zA-Z\s]+$/)
.withMessage('First name can only contain letters and spaces'),
body('lastName')
.isLength({ min: 1, max: 50 })
.withMessage('Last name must be between 1 and 50 characters')
.matches(/^[a-zA-Z\s]+$/)
.withMessage('Last name can only contain letters and spaces')
];
// 用户登录验证规则
const validateUserLogin = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Please provide a valid email address'),
body('password')
.notEmpty()
.withMessage('Password is required')
];
// 验证错误处理中间件
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
module.exports = {
validateUserRegistration,
validateUserLogin,
handleValidationErrors
};
错误处理机制
统一错误处理中间件
创建middleware/errorHandler.js:
const errorHandler = (err, req, res, next) => {
console.error('Error occurred:', {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
// 根据错误类型返回不同状态码
let error = {
success: false,
message: 'Internal Server Error'
};
if (err.name === 'ValidationError') {
error.statusCode = 400;
error.message = 'Validation Error';
error.errors = Object.values(err.errors).map(e => e.message);
} else if (err.name === 'CastError') {
error.statusCode = 400;
error.message = 'Invalid ID format';
} else if (err.code === 11000) {
error.statusCode = 409;
error.message = 'Duplicate field value entered';
} else if (err.name === 'JsonWebTokenError') {
error.statusCode = 401;
error.message = 'Invalid token';
} else if (err.name === 'TokenExpiredError') {
error.statusCode = 401;
error.message = 'Token expired';
} else {
error.statusCode = err.statusCode || 500;
}
// 开发环境返回详细错误信息
if (process.env.NODE_ENV === 'development') {
error.stack = err.stack;
}
res.status(error.statusCode).json(error);
};
module.exports = errorHandler;
自定义错误类
创建utils/customErrors.js:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// 自定义业务错误
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
}
}
class UnauthorizedError extends AppError {
constructor(message) {
super(message, 401);
}
}
class ForbiddenError extends AppError {
constructor(message) {
super(message, 403);
}
}
class ConflictError extends AppError {
constructor(message) {
super(message, 409);
}
}
module.exports = {
AppError,
ValidationError,
NotFoundError,
UnauthorizedError,
ForbiddenError,
ConflictError
};
性能优化与安全增强
缓存策略
创建middleware/cache.js:
const redis = require('redis');
const client = redis.createClient();
// 连接错误处理
client.on('error', (err) => {
console.error('Redis client error:', err);
});
// 缓存中间件
const cache = (duration = 300) => {
return async (req, res, next) => {
try {
const key = `cache:${req.originalUrl}`;
// 尝试从缓存获取数据
const cachedData = await client.get(key);
if (cachedData) {
console.log(`Cache hit for ${req.originalUrl}`);
return res.json(JSON.parse(cachedData));
}
// 如果没有缓存,包装响应函数
const originalSend = res.json;
res.json = function(data) {
// 设置缓存
client.setex(key, duration, JSON.stringify(data));
return originalSend.call(this, data);
};
next();
} catch (error) {
console.error('Cache middleware error:', error);
next();
}
};
};
module.exports = cache;
API限流
创建middleware/rateLimit.js:
const rateLimit = require('express-rate-limit');
// API请求限流
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 100个请求
message: {
success: false,
message: 'Too many requests from this IP, please try again later.'
},
standardHeaders: true,
legacyHeaders: false
});
// 登录请求限流
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 限制每个IP 5个登录尝试
message: {
success: false,
message: 'Too many login attempts, please try again later.'
},
standardHeaders: true,
legacyHeaders: false
});
module.exports = { apiLimiter, loginLimiter };
完整的项目入口文件
主服务器文件
更新server.js:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const mongoose = require('mongoose');
const connectDB = require('./config/database');
const errorHandler = require('./middleware/errorHandler');
// 路由导入
const userRoutes = require('./routes/users');
// 创建Express应用
const app = express();
// 连接数据库
connectDB();
// 中间件配置
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 健康检查端点
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
service: 'API Server'
});
});
// API路由
app.use('/api/v1/users', userRoutes);
// 错误处理中间件
app.use(errorHandler);
// 404处理
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'The requested endpoint does not exist'
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
启动脚本配置
更新package.json:
{
"name": "node-api-project",
"version": "1.0.0",
"description": "Node.js RESTful API with Express and MongoDB",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest --watch",
"lint": "eslint . --ext .js",
"format": "prettier --write ."
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.5.0",
"dotenv": "^16.3.1",
"cors": "^2.8.5",
"helmet": "^7.0.0",
"morgan": "^1.10.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"express-validator": "^7.0.1",
"redis": "^4.6.12"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.6.2",
"supertest": "^6.3.3",
"eslint": "^8.45.0",
"prettier": "^3.0.0"
}
}
测试策略
单元测试示例
创建tests/user.test.js:
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
describe('User API', () => {
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/v1/users/register', () => {
it('should register a new user', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'TestPass123',
firstName: 'Test',
lastName: 'User'
};
const response = await request(app)
.post('/api/v1/users/register')
.send(userData)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user.username).toBe(userData.username);
expect(response.body.data.user.email).toBe(userData.email);
expect(response.body.data.token).toBeDefined();
});
it('should return validation error for invalid data', async () => {
const userData = {
username: 'ab',
email: 'invalid-email',
password: '123',
firstName: '',
lastName: ''
};
const response = await request(app)
.post('/api/v1/users/register')
.send(userData)
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('POST /api/v1/users/login', () => {
beforeEach(async () => {
const userData = {
username: 'testuser',
email: '
评论 (0)