← 返回首页

核心实践

📁
模块化架构

分层设计,职责分离,高内聚低耦合,易于维护和扩展

⚙️
配置管理

环境变量驱动,支持多环境配置切换,敏感信息安全存储

📝
日志系统

结构化日志,分级输出,便于排查问题和监控分析

💓
健康检查

服务状态监控,自动故障检测,支持优雅关闭

🛡️
错误处理

统一错误处理,区分业务错误和系统错误,友好的错误响应

🧪
测试覆盖

单元测试、集成测试、端到端测试,保证代码质量

推荐的项目结构

项目目录结构
project/ ├── src/ │ ├── app.js # Express 应用配置 │ ├── server.js # HTTP 服务器启动 │ │ │ ├── config/ # 配置模块 │ │ ├── index.js # 配置聚合导出 │ │ ├── database.js # 数据库配置 │ │ ├── redis.js # Redis 配置 │ │ └── constants.js # 常量定义 │ │ │ ├── controllers/ # 控制器层 │ │ ├── authController.js │ │ └── userController.js │ │ │ ├── services/ # 业务逻辑层 │ │ ├── authService.js │ │ └── userService.js │ │ │ ├── models/ # 数据模型层 │ │ ├── index.js │ │ └── User.js │ │ │ ├── routes/ # 路由定义 │ │ ├── index.js │ │ └── v1/ │ │ ├── auth.js │ │ └── users.js │ │ │ ├── middleware/ # 中间件 │ │ ├── auth.js │ │ ├── errorHandler.js │ │ ├── rateLimiter.js │ │ └── validator.js │ │ │ ├── utils/ # 工具函数 │ │ ├── logger.js │ │ ├── response.js │ │ ├── crypto.js │ │ └── helpers.js │ │ │ └── jobs/ # 定时任务 │ └── cleanup.js │ ├── tests/ # 测试文件 │ ├── unit/ │ ├── integration/ │ └── fixtures/ │ ├── scripts/ # 脚本文件 │ ├── seed.js │ └── migrate.js │ ├── .env.example # 环境变量示例 ├── .eslintrc.js # ESLint 配置 ├── .prettierrc # Prettier 配置 ├── jest.config.js # Jest 配置 ├── package.json ├── Dockerfile └── docker-compose.yml

配置管理

环境变量配置

使用 dotenv 管理环境变量,支持开发、测试、生产多环境配置。

.env.example
# =========================================== # 应用配置 # =========================================== NODE_ENV=development PORT=3000 API_VERSION=v1 # =========================================== # 数据库配置 # =========================================== MONGODB_URI=mongodb://localhost:27017/myapp REDIS_URL=redis://localhost:6379 # =========================================== # 安全配置 # =========================================== JWT_SECRET=your-super-secret-jwt-key JWT_EXPIRES_IN=7d BCRYPT_ROUNDS=12 # =========================================== # 日志配置 # =========================================== LOG_LEVEL=info LOG_FORMAT=json # =========================================== # 限流配置 # =========================================== RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_MAX=100 # =========================================== # 第三方服务 # =========================================== SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER= SMTP_PASS= # =========================================== # 云存储 # =========================================== OSS_ACCESS_KEY= OSS_SECRET_KEY= OSS_BUCKET=

配置模块实现

config/index.js
const path = require('path'); // 根据环境加载对应的 .env 文件 const envFile = process.env.NODE_ENV === 'test' ? '.env.test' : '.env'; require('dotenv').config({ path: path.resolve(process.cwd(), envFile) }); // 必需的环境变量 const requiredEnvVars = [ 'MONGODB_URI', 'JWT_SECRET' ]; // 验证必需的环境变量 for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { throw new Error(`缺少必需的环境变量: ${envVar}`); } } const config = { // 环境 env: process.env.NODE_ENV || 'development', isDev: process.env.NODE_ENV === 'development', isProd: process.env.NODE_ENV === 'production', isTest: process.env.NODE_ENV === 'test', // 服务器 server: { port: parseInt(process.env.PORT, 10) || 3000, apiVersion: process.env.API_VERSION || 'v1' }, // 数据库 database: { mongodb: { uri: process.env.MONGODB_URI, options: { maxPoolSize: parseInt(process.env.MONGO_POOL_SIZE, 10) || 10 } }, redis: { url: process.env.REDIS_URL } }, // 安全 security: { jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || '7d' }, bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12 }, // 日志 logging: { level: process.env.LOG_LEVEL || 'info', format: process.env.LOG_FORMAT || 'json' }, // 限流 rateLimit: { windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 900000, max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100 } }; // 冻结配置,防止运行时修改 module.exports = Object.freeze(config);

日志系统

使用 Winston 日志库

utils/logger.js
const winston = require('winston'); const path = require('path'); const config = require('../config'); // 自定义日志格式 const customFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.printf(({ timestamp, level, message, stack, ...meta }) => { let log = `${timestamp} [${level.toUpperCase()}] ${message}`; if (Object.keys(meta).length) { log += ` ${JSON.stringify(meta)}`; } if (stack) { log += `\n${stack}`; } return log; }) ); // JSON 格式(生产环境) const jsonFormat = winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ); // 创建 logger 实例 const logger = winston.createLogger({ level: config.logging.level, format: config.isProd ? jsonFormat : customFormat, defaultMeta: { service: 'myapp' }, transports: [ // 控制台输出 new winston.transports.Console({ format: config.isDev ? winston.format.combine( winston.format.colorize(), customFormat ) : jsonFormat }), // 错误日志文件 new winston.transports.File({ filename: path.join('logs', 'error.log'), level: 'error', maxsize: 5242880, // 5MB maxFiles: 5 }), // 所有日志文件 new winston.transports.File({ filename: path.join('logs', 'combined.log'), maxsize: 5242880, maxFiles: 5 }) ] }); // 请求日志中间件 logger.requestLogger = (req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.info('HTTP Request', { method: req.method, url: req.originalUrl, status: res.statusCode, duration: `${duration}ms`, ip: req.ip, userAgent: req.get('user-agent') }); }); next(); }; module.exports = logger;

日志级别说明

级别 优先级 使用场景
error 0 错误信息,需要立即处理
warn 1 警告信息,潜在问题
info 2 重要业务信息
http 3 HTTP 请求日志
debug 4 调试信息,开发时使用

错误处理

自定义错误类

utils/errors.js
// 基础应用错误类 class AppError extends Error { constructor(message, statusCode, code = 'INTERNAL_ERROR') { super(message); this.statusCode = statusCode; this.code = code; this.isOperational = true; // 可操作错误(预期内的) Error.captureStackTrace(this, this.constructor); } } // 400 - 请求错误 class BadRequestError extends AppError { constructor(message = '请求参数错误') { super(message, 400, 'BAD_REQUEST'); } } // 401 - 未认证 class UnauthorizedError extends AppError { constructor(message = '请先登录') { super(message, 401, 'UNAUTHORIZED'); } } // 403 - 无权限 class ForbiddenError extends AppError { constructor(message = '没有权限执行此操作') { super(message, 403, 'FORBIDDEN'); } } // 404 - 资源不存在 class NotFoundError extends AppError { constructor(resource = '资源') { super(`${resource}不存在`, 404, 'NOT_FOUND'); } } // 409 - 冲突 class ConflictError extends AppError { constructor(message = '资源已存在') { super(message, 409, 'CONFLICT'); } } // 422 - 验证错误 class ValidationError extends AppError { constructor(errors) { super('数据验证失败', 422, 'VALIDATION_ERROR'); this.errors = errors; } } // 429 - 请求过多 class TooManyRequestsError extends AppError { constructor(message = '请求过于频繁,请稍后再试') { super(message, 429, 'TOO_MANY_REQUESTS'); } } module.exports = { AppError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, ValidationError, TooManyRequestsError };

全局错误处理中间件

middleware/errorHandler.js
const logger = require('../utils/logger'); const config = require('../config'); const { AppError } = require('../utils/errors'); // 处理 Mongoose 验证错误 const handleMongooseValidationError = (err) => { const errors = Object.values(err.errors).map(e => ({ field: e.path, message: e.message })); return new AppError('数据验证失败', 422, 'VALIDATION_ERROR'); }; // 处理 MongoDB 重复键错误 const handleDuplicateKeyError = (err) => { const field = Object.keys(err.keyValue)[0]; return new AppError(`${field} 已存在`, 409, 'CONFLICT'); }; // 处理 JWT 错误 const handleJWTError = () => new AppError('无效的认证令牌', 401, 'INVALID_TOKEN'); const handleJWTExpiredError = () => new AppError('认证令牌已过期', 401, 'TOKEN_EXPIRED'); // 错误处理中间件 const errorHandler = (err, req, res, next) => { let error = { ...err }; error.message = err.message; // 转换特定错误类型 if (err.name === 'ValidationError') { error = handleMongooseValidationError(err); } if (err.code === 11000) { error = handleDuplicateKeyError(err); } if (err.name === 'JsonWebTokenError') { error = handleJWTError(); } if (err.name === 'TokenExpiredError') { error = handleJWTExpiredError(); } // 记录错误日志 if (!error.isOperational) { logger.error('未处理的错误', { error: err.message, stack: err.stack, path: req.path, method: req.method }); } // 发送错误响应 const statusCode = error.statusCode || 500; const response = { success: false, error: { code: error.code || 'INTERNAL_ERROR', message: error.isOperational ? error.message : '服务器内部错误' } }; // 开发环境返回错误堆栈 if (config.isDev) { response.error.stack = err.stack; } // 验证错误返回详细信息 if (error.errors) { response.error.details = error.errors; } res.status(statusCode).json(response); }; module.exports = errorHandler;

健康检查

健康检查接口

routes/health.js
const express = require('express'); const mongoose = require('mongoose'); const redis = require('../config/redis'); const router = express.Router(); // 简单健康检查(用于负载均衡器) router.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }); // 就绪检查(检查所有依赖服务) router.get('/health/ready', async (req, res) => { const checks = { status: 'ok', timestamp: new Date().toISOString(), services: {} }; // 检查 MongoDB try { const mongoState = mongoose.connection.readyState; checks.services.mongodb = { status: mongoState === 1 ? 'healthy' : 'unhealthy', responseTime: null }; if (mongoState === 1) { const start = Date.now(); await mongoose.connection.db.admin().ping(); checks.services.mongodb.responseTime = Date.now() - start + 'ms'; } } catch (err) { checks.services.mongodb = { status: 'unhealthy', error: err.message }; checks.status = 'degraded'; } // 检查 Redis try { const start = Date.now(); await redis.ping(); checks.services.redis = { status: 'healthy', responseTime: Date.now() - start + 'ms' }; } catch (err) { checks.services.redis = { status: 'unhealthy', error: err.message }; checks.status = 'degraded'; } const statusCode = checks.status === 'ok' ? 200 : 503; res.status(statusCode).json(checks); }); // 详细状态(包含系统信息) router.get('/health/detail', (req, res) => { const memUsage = process.memoryUsage(); res.json({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime() + 's', memory: { heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB', heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB', rss: Math.round(memUsage.rss / 1024 / 1024) + 'MB' }, cpu: process.cpuUsage(), pid: process.pid, nodeVersion: process.version }); }); module.exports = router;

优雅关闭

server.js
const http = require('http'); const mongoose = require('mongoose'); const app = require('./app'); const logger = require('./utils/logger'); const config = require('./config'); const server = http.createServer(app); // 优雅关闭函数 const gracefulShutdown = async (signal) => { logger.info(`收到 ${signal} 信号,开始优雅关闭...`); // 停止接收新请求 server.close(async () => { logger.info('HTTP 服务器已关闭'); try { // 关闭数据库连接 await mongoose.connection.close(); logger.info('MongoDB 连接已关闭'); // 关闭 Redis 连接 // await redis.quit(); logger.info('优雅关闭完成'); process.exit(0); } catch (err) { logger.error('关闭过程中发生错误', err); process.exit(1); } }); // 强制关闭超时 setTimeout(() => { logger.error('强制关闭(超时)'); process.exit(1); }, 30000); }; // 监听关闭信号 process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); // 未捕获的异常 process.on('uncaughtException', (err) => { logger.error('未捕获的异常', err); process.exit(1); }); // 未处理的 Promise 拒绝 process.on('unhandledRejection', (reason, promise) => { logger.error('未处理的 Promise 拒绝', { reason }); }); // 启动服务器 server.listen(config.server.port, () => { logger.info(`服务器运行在端口 ${config.server.port}`); });

数据验证

使用 Joi 进行数据验证

validators/userValidator.js
const Joi = require('joi'); const userSchemas = { // 创建用户 create: Joi.object({ username: Joi.string() .min(2) .max(20) .pattern(/^[a-zA-Z0-9_]+$/) .required() .messages({ 'string.min': '用户名至少2个字符', 'string.max': '用户名最多20个字符', 'string.pattern.base': '用户名只能包含字母、数字和下划线' }), email: Joi.string() .email() .required() .messages({ 'string.email': '请输入有效的邮箱地址' }), password: Joi.string() .min(6) .max(50) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) .required() .messages({ 'string.pattern.base': '密码必须包含大小写字母和数字' }), confirmPassword: Joi.string() .valid(Joi.ref('password')) .required() .messages({ 'any.only': '两次密码输入不一致' }) }), // 更新用户 update: Joi.object({ username: Joi.string().min(2).max(20), email: Joi.string().email(), bio: Joi.string().max(200) }).min(1), // 至少更新一个字段 // 查询参数 query: Joi.object({ page: Joi.number().integer().min(1).default(1), limit: Joi.number().integer().min(1).max(100).default(10), sort: Joi.string().valid('createdAt', '-createdAt', 'username'), search: Joi.string().max(50) }) }; module.exports = userSchemas;

验证中间件

middleware/validate.js
const { ValidationError } = require('../utils/errors'); const validate = (schema, property = 'body') => { return (req, res, next) => { const { error, value } = schema.validate(req[property], { abortEarly: false, // 返回所有错误 stripUnknown: true // 移除未定义的字段 }); if (error) { const errors = error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message })); return next(new ValidationError(errors)); } req[property] = value; next(); }; }; module.exports = validate;

测试策略

单元测试示例

tests/unit/userService.test.js
const userService = require('../../src/services/userService'); const User = require('../../src/models/User'); // Mock User 模型 jest.mock('../../src/models/User'); describe('UserService', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('createUser', () => { it('应该成功创建用户', async () => { const userData = { username: 'testuser', email: 'test@example.com', password: 'Password123' }; const mockUser = { _id: '123', ...userData }; User.create.mockResolvedValue(mockUser); const result = await userService.createUser(userData); expect(User.create).toHaveBeenCalledWith(userData); expect(result).toEqual(mockUser); }); it('用户名已存在时应该抛出错误', async () => { User.create.mockRejectedValue({ code: 11000 }); await expect( userService.createUser({ username: 'existing' }) ).rejects.toThrow(); }); }); describe('getUserById', () => { it('应该返回用户信息', async () => { const mockUser = { _id: '123', username: 'test' }; User.findById.mockReturnValue({ select: jest.fn().mockResolvedValue(mockUser) }); const result = await userService.getUserById('123'); expect(result).toEqual(mockUser); }); it('用户不存在时应该返回 null', async () => { User.findById.mockReturnValue({ select: jest.fn().mockResolvedValue(null) }); const result = await userService.getUserById('nonexistent'); expect(result).toBeNull(); }); }); });

集成测试示例

tests/integration/auth.test.js
const request = require('supertest'); const app = require('../../src/app'); const User = require('../../src/models/User'); const { setupTestDB, teardownTestDB } = require('../helpers'); describe('Auth API', () => { beforeAll(async () => { await setupTestDB(); }); afterAll(async () => { await teardownTestDB(); }); beforeEach(async () => { await User.deleteMany({}); }); describe('POST /api/v1/auth/register', () => { it('应该成功注册新用户', async () => { const res = await request(app) .post('/api/v1/auth/register') .send({ username: 'testuser', email: 'test@example.com', password: 'Password123', confirmPassword: 'Password123' }); expect(res.status).toBe(201); expect(res.body.success).toBe(true); expect(res.body.data.token).toBeDefined(); }); it('邮箱已存在时应该返回 409', async () => { await User.create({ username: 'existing', email: 'test@example.com', password: 'Password123' }); const res = await request(app) .post('/api/v1/auth/register') .send({ username: 'newuser', email: 'test@example.com', password: 'Password123', confirmPassword: 'Password123' }); expect(res.status).toBe(409); }); }); });

安全实践

安全检查清单

  • 使用 HTTPS - 生产环境必须启用 SSL/TLS
  • 设置安全 HTTP 头 - 使用 helmet 中间件
  • 防止注入攻击 - 参数化查询,验证输入
  • 密码安全存储 - 使用 bcrypt 哈希,足够的 salt rounds
  • 实现请求限流 - 防止暴力攻击和 DDoS
  • 敏感数据加密 - 加密存储敏感信息
  • 定期更新依赖 - 使用 npm audit 检查漏洞
  • 日志脱敏 - 不记录密码等敏感信息
⚠️ 安全提醒

永远不要在代码中硬编码密钥、密码等敏感信息。使用环境变量或专门的密钥管理服务。