express-production
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExpress.js - Production Web Framework
Express.js - 生产级Web框架
Overview
概述
Express is a minimal and flexible Node.js web application framework providing a robust set of features for web and mobile applications. This skill covers production-ready Express development including middleware architecture, structured error handling, security hardening, comprehensive testing, and deployment strategies.
Key Features:
- Flexible middleware architecture with composition patterns
- Centralized error handling with async support
- Security hardening (Helmet, CORS, rate limiting, input validation)
- Comprehensive testing with Supertest
- Production deployment with PM2 clustering
- Environment-based configuration
- Structured logging and monitoring
- Graceful shutdown patterns
- Zero-downtime deployments
Installation:
bash
undefinedExpress是一个轻量灵活的Node.js Web应用框架,为Web和移动应用提供了一套强大的功能。本指南涵盖面向生产环境的Express开发内容,包括中间件架构、结构化错误处理、安全加固、全面测试及部署策略。
核心特性:
- 支持组合模式的灵活中间件架构
- 支持异步的集中式错误处理
- 安全加固(Helmet、CORS、速率限制、输入验证)
- 基于Supertest的全面测试方案
- 基于PM2集群的生产环境部署
- 基于环境的配置管理
- 结构化日志与监控
- 优雅停机模式
- 零停机部署
安装:
bash
undefinedBasic Express
基础Express
npm install express
npm install express
Production stack
生产环境依赖栈
npm install express helmet cors express-rate-limit express-validator
npm install morgan winston compression
npm install dotenv
npm install express helmet cors express-rate-limit express-validator
npm install morgan winston compression
npm install dotenv
Development tools
开发工具
npm install -D nodemon supertest jest
npm install -D nodemon supertest jest
Optional: Database and auth
可选:数据库与认证
npm install mongoose jsonwebtoken bcrypt
undefinednpm install mongoose jsonwebtoken bcrypt
undefinedWhen to Use This Skill
何时使用本指南
Use this comprehensive Express skill when:
- Building production REST APIs
- Creating microservices architectures
- Implementing secure web applications
- Need flexible middleware composition
- Require comprehensive error handling
- Building systems requiring extensive testing
- Deploying high-availability services
- Need granular control over request/response lifecycle
Express vs Other Frameworks:
- Express: Maximum flexibility, unopinionated, extensive ecosystem
- Fastify: Performance-focused, schema-based validation
- Koa: Modern async/await, minimalist
- NestJS: TypeScript-first, opinionated, enterprise patterns
在以下场景中使用本全面的Express开发指南:
- 构建生产级REST API
- 创建微服务架构
- 实现安全的Web应用
- 需要灵活的中间件组合
- 要求完善的错误处理机制
- 构建需要全面测试的系统
- 部署高可用服务
- 需要对请求/响应生命周期进行精细控制
Express与其他框架对比:
- Express: 灵活性最高,无约束设计,生态系统丰富
- Fastify: 性能优先,基于Schema的验证
- Koa: 现代异步/await设计,极简风格
- NestJS: TypeScript优先,约束式设计,企业级模式
Quick Start
快速开始
Minimal Express Server
极简Express服务器
javascript
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
// Start server
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing server...');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});Run Development Server:
bash
undefinedjavascript
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 路由
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
// 错误处理器
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
// 启动服务器
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// 优雅停机
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing server...');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});启动开发服务器:
bash
undefinedInstall nodemon
安装nodemon
npm install -D nodemon
npm install -D nodemon
Run with nodemon
使用nodemon启动
npx nodemon server.js
npx nodemon server.js
Or add to package.json
或添加到package.json
npm run dev
undefinednpm run dev
undefinedProduction-Ready Server Structure
生产级服务器结构
project/
├── src/
│ ├── app.js # Express app factory
│ ├── server.js # Server entry point
│ ├── config/
│ │ ├── index.js # Configuration management
│ │ └── logger.js # Winston logger setup
│ ├── middleware/
│ │ ├── errorHandler.js # Centralized error handling
│ │ ├── validation.js # Input validation
│ │ ├── auth.js # Authentication middleware
│ │ └── rateLimiter.js # Rate limiting
│ ├── routes/
│ │ ├── index.js # Route aggregator
│ │ ├── users.js # User routes
│ │ └── api/ # API versioning
│ ├── controllers/
│ │ ├── userController.js
│ │ └── authController.js
│ ├── models/ # Data models
│ ├── services/ # Business logic
│ ├── utils/
│ │ ├── AppError.js # Custom error class
│ │ └── catchAsync.js # Async wrapper
│ └── tests/
│ ├── unit/
│ └── integration/
├── ecosystem.config.js # PM2 configuration
├── .env.example # Environment template
├── nodemon.json # Nodemon config
└── package.jsonproject/
├── src/
│ ├── app.js # Express应用工厂
│ ├── server.js # 服务器入口文件
│ ├── config/
│ │ ├── index.js # 配置管理
│ │ └── logger.js # Winston日志配置
│ ├── middleware/
│ │ ├── errorHandler.js # 集中式错误处理
│ │ ├── validation.js # 输入验证
│ │ ├── auth.js # 认证中间件
│ │ └── rateLimiter.js # 速率限制
│ ├── routes/
│ │ ├── index.js # 路由聚合器
│ │ ├── users.js # 用户路由
│ │ └── api/ # API版本控制
│ ├── controllers/
│ │ ├── userController.js
│ │ └── authController.js
│ ├── models/ # 数据模型
│ ├── services/ # 业务逻辑
│ ├── utils/
│ │ ├── AppError.js # 自定义错误类
│ │ └── catchAsync.js # 异步包装器
│ └── tests/
│ ├── unit/
│ └── integration/
├── ecosystem.config.js # PM2配置
├── .env.example # 环境变量模板
├── nodemon.json # Nodemon配置
└── package.jsonMiddleware Architecture
中间件架构
Understanding Middleware
理解中间件
Middleware functions are functions that have access to the request object (), response object (), and the next middleware function ().
reqresnextMiddleware Types:
- Application-level: or
app.use()app.METHOD() - Router-level: or
router.use()router.METHOD() - Error-handling: Four parameters
(err, req, res, next) - Built-in: ,
express.json()express.static() - Third-party: ,
helmet,corsmorgan
中间件函数可以访问请求对象()、响应对象()以及下一个中间件函数()。
reqresnext中间件类型:
- 应用级: 或
app.use()app.METHOD() - 路由级: 或
router.use()router.METHOD() - 错误处理: 四个参数
(err, req, res, next) - 内置: 、
express.json()express.static() - 第三方: 、
helmet、corsmorgan
Proper Middleware Order
正确的中间件顺序
✅ Correct Order:
javascript
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const app = express();
// 1. Security headers (FIRST)
app.use(helmet());
// 2. CORS configuration
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// 3. Rate limiting (before parsing)
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// 4. Request parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 5. Compression
app.use(compression());
// 6. Logging
if (process.env.NODE_ENV !== 'production') {
app.use(morgan('dev'));
} else {
app.use(morgan('combined'));
}
// 7. Static files (if needed)
app.use(express.static('public'));
// 8. Custom middleware
app.use(require('./middleware/requestId'));
app.use(require('./middleware/timing'));
// 9. Routes
app.use('/api/v1/users', require('./routes/users'));
app.use('/api/v1/posts', require('./routes/posts'));
// 10. 404 handler (after all routes)
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// 11. Error handling (LAST)
app.use(require('./middleware/errorHandler'));❌ Wrong Order:
javascript
// DON'T: Routes before security
app.use('/api/users', userRoutes); // Routes first
app.use(helmet()); // Security too late!
// DON'T: Error handler before routes
app.use(errorHandler); // Error handler first
app.use('/api/users', userRoutes); // Routes won't be caught
// DON'T: Parsing after routes
app.use('/api/users', userRoutes);
app.use(express.json()); // Too late to parse!✅ 正确顺序:
javascript
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const app = express();
// 1. 安全头(优先级最高)
app.use(helmet());
// 2. CORS配置
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// 3. 速率限制(在解析之前)
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP在窗口时间内最多100次请求
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// 4. 请求解析
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 5. 压缩
app.use(compression());
// 6. 日志
if (process.env.NODE_ENV !== 'production') {
app.use(morgan('dev'));
} else {
app.use(morgan('combined'));
}
// 7. 静态文件(如果需要)
app.use(express.static('public'));
// 8. 自定义中间件
app.use(require('./middleware/requestId'));
app.use(require('./middleware/timing'));
// 9. 路由
app.use('/api/v1/users', require('./routes/users'));
app.use('/api/v1/posts', require('./routes/posts'));
// 10. 404处理器(所有路由之后)
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// 11. 错误处理(最后)
app.use(require('./middleware/errorHandler'));❌ 错误顺序:
javascript
// 错误:路由在安全中间件之前
app.use('/api/users', userRoutes); // 路由优先
app.use(helmet()); // 安全中间件太晚!
// 错误:错误处理器在路由之前
app.use(errorHandler); // 错误处理器优先
app.use('/api/users', userRoutes); // 路由不会被捕获
// 错误:解析中间件在路由之后
app.use('/api/users', userRoutes);
app.use(express.json()); // 解析太晚!Custom Middleware Patterns
自定义中间件模式
Request ID Middleware:
javascript
// middleware/requestId.js
const { v4: uuidv4 } = require('uuid');
module.exports = function requestId(req, res, next) {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
};Request Timing Middleware:
javascript
// middleware/timing.js
module.exports = function timing(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} - ${duration}ms`);
});
next();
};Authentication Middleware:
javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
const AppError = require('../utils/AppError');
exports.authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return next(new AppError('No token provided', 401));
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
next(new AppError('Invalid token', 401));
}
};
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return next(new AppError('Not authenticated', 401));
}
if (!roles.includes(req.user.role)) {
return next(new AppError('Insufficient permissions', 403));
}
next();
};
};Usage:
javascript
const { authenticate, authorize } = require('./middleware/auth');
// Public route
app.get('/api/posts', getPosts);
// Authenticated route
app.get('/api/profile', authenticate, getProfile);
// Role-based authorization
app.delete('/api/users/:id',
authenticate,
authorize('admin', 'moderator'),
deleteUser
);请求ID中间件:
javascript
// middleware/requestId.js
const { v4: uuidv4 } = require('uuid');
module.exports = function requestId(req, res, next) {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
};请求计时中间件:
javascript
// middleware/timing.js
module.exports = function timing(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} - ${duration}ms`);
});
next();
};认证中间件:
javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
const AppError = require('../utils/AppError');
exports.authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return next(new AppError('No token provided', 401));
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
next(new AppError('Invalid token', 401));
}
};
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return next(new AppError('Not authenticated', 401));
}
if (!roles.includes(req.user.role)) {
return next(new AppError('Insufficient permissions', 403));
}
next();
};
};使用方式:
javascript
const { authenticate, authorize } = require('./middleware/auth');
// 公开路由
app.get('/api/posts', getPosts);
// 需认证的路由
app.get('/api/profile', authenticate, getProfile);
// 基于角色的授权
app.delete('/api/users/:id',
authenticate,
authorize('admin', 'moderator'),
deleteUser
);Async Middleware
异步中间件
✅ Correct Async Handling:
javascript
// utils/catchAsync.js
module.exports = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
// Usage
const catchAsync = require('../utils/catchAsync');
app.get('/users', catchAsync(async (req, res) => {
const users = await User.find();
res.json({ users });
}));❌ Wrong: No Error Handling:
javascript
// DON'T: Async without catch
app.get('/users', async (req, res) => {
const users = await User.find(); // Unhandled rejection!
res.json({ users });
});✅ 正确的异步处理:
javascript
// utils/catchAsync.js
module.exports = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
// 使用方式
const catchAsync = require('../utils/catchAsync');
app.get('/users', catchAsync(async (req, res) => {
const users = await User.find();
res.json({ users });
}));❌ 错误示例:无错误处理:
javascript
// 错误:异步函数无catch
app.get('/users', async (req, res) => {
const users = await User.find(); // 未处理的拒绝!
res.json({ users });
});Middleware Composition
中间件组合
Compose Multiple Middleware:
javascript
// middleware/compose.js
const compose = (...middleware) => {
return (req, res, next) => {
let index = 0;
const dispatch = (i) => {
if (i >= middleware.length) return next();
const fn = middleware[i];
try {
fn(req, res, () => dispatch(i + 1));
} catch (err) {
next(err);
}
};
dispatch(0);
};
};
// Usage
const adminOnly = compose(
authenticate,
authorize('admin'),
validateRequest
);
app.delete('/api/users/:id', adminOnly, deleteUser);Conditional Middleware:
javascript
// Apply middleware conditionally
const conditionalMiddleware = (condition, middleware) => {
return (req, res, next) => {
if (condition(req)) {
return middleware(req, res, next);
}
next();
};
};
// Only log in development
app.use(conditionalMiddleware(
(req) => process.env.NODE_ENV === 'development',
morgan('dev')
));组合多个中间件:
javascript
// middleware/compose.js
const compose = (...middleware) => {
return (req, res, next) => {
let index = 0;
const dispatch = (i) => {
if (i >= middleware.length) return next();
const fn = middleware[i];
try {
fn(req, res, () => dispatch(i + 1));
} catch (err) {
next(err);
}
};
dispatch(0);
};
};
// 使用方式
const adminOnly = compose(
authenticate,
authorize('admin'),
validateRequest
);
app.delete('/api/users/:id', adminOnly, deleteUser);条件中间件:
javascript
// 条件式应用中间件
const conditionalMiddleware = (condition, middleware) => {
return (req, res, next) => {
if (condition(req)) {
return middleware(req, res, next);
}
next();
};
};
// 仅在开发环境中记录日志
app.use(conditionalMiddleware(
(req) => process.env.NODE_ENV === 'development',
morgan('dev')
));Structured Error Handling
结构化错误处理
Custom Error Classes
自定义错误类
javascript
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;Error Hierarchy:
javascript
// utils/errors.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400);
this.errors = errors;
}
}
class AuthenticationError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401);
}
}
class AuthorizationError extends AppError {
constructor(message = 'Insufficient permissions') {
super(message, 403);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409);
}
}
module.exports = {
AppError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError
};javascript
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;错误层级:
javascript
// utils/errors.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400);
this.errors = errors;
}
}
class AuthenticationError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401);
}
}
class AuthorizationError extends AppError {
constructor(message = 'Insufficient permissions') {
super(message, 403);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409);
}
}
module.exports = {
AppError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError
};Centralized Error Handler
集中式错误处理器
javascript
// middleware/errorHandler.js
const logger = require('../config/logger');
function errorHandler(err, req, res, next) {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// Log error
logger.error({
message: err.message,
statusCode: err.statusCode,
stack: err.stack,
path: req.path,
method: req.method,
ip: req.ip,
userId: req.user?.id
});
// Development: send full error
if (process.env.NODE_ENV === 'development') {
return res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
}
// Production: sanitize errors
if (err.isOperational) {
// Operational, trusted error: send to client
return res.status(err.statusCode).json({
status: err.status,
message: err.message,
...(err.errors && { errors: err.errors })
});
}
// Programming or unknown error: don't leak details
console.error('ERROR 💥', err);
return res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
}
module.exports = errorHandler;javascript
// middleware/errorHandler.js
const logger = require('../config/logger');
function errorHandler(err, req, res, next) {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// 记录错误
logger.error({
message: err.message,
statusCode: err.statusCode,
stack: err.stack,
path: req.path,
method: req.method,
ip: req.ip,
userId: req.user?.id
});
// 开发环境:返回完整错误
if (process.env.NODE_ENV === 'development') {
return res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
}
// 生产环境:清理错误信息
if (err.isOperational) {
// 可预期的业务错误:返回给客户端
return res.status(err.statusCode).json({
status: err.status,
message: err.message,
...(err.errors && { errors: err.errors })
});
}
// 编程错误或未知错误:不泄露细节
console.error('ERROR 💥', err);
return res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
}
module.exports = errorHandler;Handling Specific Error Types
处理特定错误类型
javascript
// middleware/errorHandler.js (extended)
function handleCastError(err) {
const message = `Invalid ${err.path}: ${err.value}`;
return new AppError(message, 400);
}
function handleDuplicateFields(err) {
const field = Object.keys(err.keyValue)[0];
const message = `Duplicate field value: ${field}. Please use another value`;
return new AppError(message, 400);
}
function handleValidationError(err) {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
}
function handleJWTError() {
return new AppError('Invalid token. Please log in again', 401);
}
function handleJWTExpiredError() {
return new AppError('Your token has expired. Please log in again', 401);
}
module.exports = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Mongoose bad ObjectId
if (err.name === 'CastError') error = handleCastError(error);
// Mongoose duplicate key
if (err.code === 11000) error = handleDuplicateFields(error);
// Mongoose validation error
if (err.name === 'ValidationError') error = handleValidationError(error);
// JWT errors
if (err.name === 'JsonWebTokenError') error = handleJWTError();
if (err.name === 'TokenExpiredError') error = handleJWTExpiredError();
// Send response
sendErrorResponse(error, req, res);
};javascript
// middleware/errorHandler.js (扩展版)
function handleCastError(err) {
const message = `Invalid ${err.path}: ${err.value}`;
return new AppError(message, 400);
}
function handleDuplicateFields(err) {
const field = Object.keys(err.keyValue)[0];
const message = `Duplicate field value: ${field}. Please use another value`;
return new AppError(message, 400);
}
function handleValidationError(err) {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
}
function handleJWTError() {
return new AppError('Invalid token. Please log in again', 401);
}
function handleJWTExpiredError() {
return new AppError('Your token has expired. Please log in again', 401);
}
module.exports = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Mongoose无效ObjectId错误
if (err.name === 'CastError') error = handleCastError(error);
// Mongoose重复键错误
if (err.code === 11000) error = handleDuplicateFields(error);
// Mongoose验证错误
if (err.name === 'ValidationError') error = handleValidationError(error);
// JWT错误
if (err.name === 'JsonWebTokenError') error = handleJWTError();
if (err.name === 'TokenExpiredError') error = handleJWTExpiredError();
// 返回响应
sendErrorResponse(error, req, res);
};Async Error Handling
异步错误处理
javascript
// utils/catchAsync.js
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
module.exports = catchAsync;
// Usage in controllers
const catchAsync = require('../utils/catchAsync');
const User = require('../models/User');
const { NotFoundError } = require('../utils/errors');
exports.getUser = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new NotFoundError('User'));
}
res.json({ user });
});
exports.createUser = catchAsync(async (req, res, next) => {
const user = await User.create(req.body);
res.status(201).json({ user });
});javascript
// utils/catchAsync.js
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
module.exports = catchAsync;
// 在控制器中使用
const catchAsync = require('../utils/catchAsync');
const User = require('../models/User');
const { NotFoundError } = require('../utils/errors');
exports.getUser = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new NotFoundError('User'));
}
res.json({ user });
});
exports.createUser = catchAsync(async (req, res, next) => {
const user = await User.create(req.body);
res.status(201).json({ user });
});Unhandled Rejections
未处理的Promise拒绝
javascript
// server.js
const app = require('./app');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION! 💥 Shutting down...');
console.error(err.name, err.message);
server.close(() => {
process.exit(1);
});
});
// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
console.error(err.name, err.message);
process.exit(1);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('👋 SIGTERM RECEIVED. Shutting down gracefully');
server.close(() => {
console.log('💥 Process terminated!');
});
});javascript
// server.js
const app = require('./app');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// 处理未处理的Promise拒绝
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION! 💥 Shutting down...');
console.error(err.name, err.message);
server.close(() => {
process.exit(1);
});
});
// 处理未捕获的异常
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
console.error(err.name, err.message);
process.exit(1);
});
// 优雅停机
process.on('SIGTERM', () => {
console.log('👋 SIGTERM RECEIVED. Shutting down gracefully');
server.close(() => {
console.log('💥 Process terminated!');
});
});Security Hardening
安全加固
Helmet.js Configuration
Helmet.js配置
javascript
// config/security.js
const helmet = require('helmet');
const securityConfig = helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
// Strict Transport Security
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
// X-Frame-Options
frameguard: {
action: 'deny'
},
// X-Content-Type-Options
noSniff: true,
// X-XSS-Protection
xssFilter: true,
// Referrer-Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
}
});
module.exports = securityConfig;Usage:
javascript
// app.js
const securityConfig = require('./config/security');
app.use(securityConfig);javascript
// config/security.js
const helmet = require('helmet');
const securityConfig = helmet({
// 内容安全策略
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
// 严格传输安全
hsts: {
maxAge: 31536000, // 1年
includeSubDomains: true,
preload: true
},
// X-Frame-Options
frameguard: {
action: 'deny'
},
// X-Content-Type-Options
noSniff: true,
// X-XSS-Protection
xssFilter: true,
// Referrer-Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
}
});
module.exports = securityConfig;使用方式:
javascript
// app.js
const securityConfig = require('./config/security');
app.use(securityConfig);CORS Configuration
CORS配置
javascript
// config/cors.js
const cors = require('cors');
const whitelist = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, Postman)
if (!origin) return callback(null, true);
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
maxAge: 86400 // 24 hours
};
module.exports = cors(corsOptions);javascript
// config/cors.js
const cors = require('cors');
const whitelist = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];
const corsOptions = {
origin: function (origin, callback) {
// 允许无origin的请求(移动应用、Postman)
if (!origin) return callback(null, true);
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
maxAge: 86400 // 24小时
};
module.exports = cors(corsOptions);Rate Limiting
速率限制
javascript
// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
// Redis client for distributed rate limiting
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
// General rate limiter
exports.generalLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:general:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later',
standardHeaders: true, // Return rate limit info in RateLimit-* headers
legacyHeaders: false // Disable X-RateLimit-* headers
});
// Strict rate limiter for auth endpoints
exports.authLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:auth:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 login attempts per windowMs
message: 'Too many login attempts, please try again later',
skipSuccessfulRequests: true // Don't count successful requests
});
// API key limiter (higher limits for authenticated users)
exports.apiKeyLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 1000,
keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
skip: (req) => !req.headers['x-api-key']
});Usage:
javascript
const { generalLimiter, authLimiter } = require('./middleware/rateLimiter');
// Apply to all routes
app.use('/api/', generalLimiter);
// Strict limiting for auth
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);javascript
// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
// Redis客户端用于分布式速率限制
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
// 通用速率限制器
exports.generalLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:general:'
}),
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP在窗口时间内最多100次请求
message: 'Too many requests from this IP, please try again later',
standardHeaders: true, // 在RateLimit-*头中返回速率限制信息
legacyHeaders: false // 禁用X-RateLimit-*头
});
// 认证端点的严格速率限制器
exports.authLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:auth:'
}),
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 限制每个IP在窗口时间内最多5次登录尝试
message: 'Too many login attempts, please try again later',
skipSuccessfulRequests: true // 不统计成功的请求
});
// API密钥速率限制器(认证用户拥有更高限制)
exports.apiKeyLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1小时
max: 1000,
keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
skip: (req) => !req.headers['x-api-key']
});使用方式:
javascript
const { generalLimiter, authLimiter } = require('./middleware/rateLimiter');
// 应用到所有路由
app.use('/api/', generalLimiter);
// 对认证端点应用严格限制
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);Input Validation and Sanitization
输入验证与清理
javascript
// middleware/validation.js
const { body, param, query, validationResult } = require('express-validator');
const { ValidationError } = require('../utils/errors');
// Validation middleware
exports.validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const extractedErrors = errors.array().map(err => ({
field: err.param,
message: err.msg,
value: err.value
}));
return next(new ValidationError('Validation failed', extractedErrors));
}
next();
};
// User validation rules
exports.createUserRules = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Must be a valid email'),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Password must contain uppercase, lowercase, and number'),
body('name')
.trim()
.notEmpty()
.withMessage('Name is required')
.isLength({ max: 100 })
.withMessage('Name too long')
.escape(), // XSS protection
body('age')
.optional()
.isInt({ min: 0, max: 150 })
.withMessage('Age must be between 0 and 150')
];
exports.updateUserRules = [
param('id')
.isMongoId()
.withMessage('Invalid user ID'),
body('email')
.optional()
.isEmail()
.normalizeEmail(),
body('name')
.optional()
.trim()
.notEmpty()
.escape()
];
// Usage
const { createUserRules, validate } = require('./middleware/validation');
app.post('/api/users', createUserRules, validate, createUser);javascript
// middleware/validation.js
const { body, param, query, validationResult } = require('express-validator');
const { ValidationError } = require('../utils/errors');
// 验证中间件
exports.validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const extractedErrors = errors.array().map(err => ({
field: err.param,
message: err.msg,
value: err.value
}));
return next(new ValidationError('Validation failed', extractedErrors));
}
next();
};
// 用户验证规则
exports.createUserRules = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Must be a valid email'),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Password must contain uppercase, lowercase, and number'),
body('name')
.trim()
.notEmpty()
.withMessage('Name is required')
.isLength({ max: 100 })
.withMessage('Name too long')
.escape(), // XSS防护
body('age')
.optional()
.isInt({ min: 0, max: 150 })
.withMessage('Age must be between 0 and 150')
];
exports.updateUserRules = [
param('id')
.isMongoId()
.withMessage('Invalid user ID'),
body('email')
.optional()
.isEmail()
.normalizeEmail(),
body('name')
.optional()
.trim()
.notEmpty()
.escape()
];
// 使用方式
const { createUserRules, validate } = require('./middleware/validation');
app.post('/api/users', createUserRules, validate, createUser);SQL Injection Prevention
SQL注入防护
javascript
// DON'T: String concatenation
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`; // Vulnerable!
// DO: Parameterized queries
const query = 'SELECT * FROM users WHERE email = ?';
connection.query(query, [req.body.email], (err, results) => {
// Safe from SQL injection
});
// DO: ORM/Query Builder
const user = await User.findOne({ email: req.body.email }); // Mongoose
const user = await db('users').where('email', req.body.email).first(); // Knexjavascript
// 错误:字符串拼接
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`; // 易受攻击!
// 正确:参数化查询
const query = 'SELECT * FROM users WHERE email = ?';
connection.query(query, [req.body.email], (err, results) => {
// 免受SQL注入
});
// 正确:ORM/查询构建器
const user = await User.findOne({ email: req.body.email }); // Mongoose
const user = await db('users').where('email', req.body.email).first(); // KnexXSS Protection
XSS防护
javascript
// Install: npm install xss-clean
const xss = require('xss-clean');
// Apply XSS sanitization
app.use(xss());
// Additional: HTML escaping in templates
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};javascript
// 安装:npm install xss-clean
const xss = require('xss-clean');
// 应用XSS清理
app.use(xss());
// 额外:模板中的HTML转义
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};Environment Variable Security
环境变量安全
javascript
// config/index.js
require('dotenv').config();
const requiredEnvVars = [
'NODE_ENV',
'PORT',
'DATABASE_URL',
'JWT_SECRET',
'REDIS_HOST'
];
// Validate required environment variables
requiredEnvVars.forEach((envVar) => {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
});
// Validate JWT_SECRET strength
if (process.env.JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET must be at least 32 characters');
}
module.exports = {
env: process.env.NODE_ENV,
port: parseInt(process.env.PORT, 10),
database: {
url: process.env.DATABASE_URL
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
},
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT, 10) || 6379
}
};javascript
// config/index.js
require('dotenv').config();
const requiredEnvVars = [
'NODE_ENV',
'PORT',
'DATABASE_URL',
'JWT_SECRET',
'REDIS_HOST'
];
// 验证必填环境变量
requiredEnvVars.forEach((envVar) => {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
});
// 验证JWT_SECRET强度
if (process.env.JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET must be at least 32 characters');
}
module.exports = {
env: process.env.NODE_ENV,
port: parseInt(process.env.PORT, 10),
database: {
url: process.env.DATABASE_URL
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
},
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT, 10) || 6379
}
};Testing with Supertest
测试(使用Supertest)
Test Setup
测试配置
javascript
// tests/setup.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
// Setup before all tests
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});
// Cleanup after each test
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
});
// Teardown after all tests
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});javascript
// tests/setup.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
// 所有测试前的配置
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});
// 每个测试后的清理
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
});
// 所有测试后的清理
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});Integration Testing
集成测试
javascript
// tests/integration/users.test.js
const request = require('supertest');
const app = require('../../src/app');
const User = require('../../src/models/User');
describe('User API', () => {
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'Password123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toHaveProperty('user');
expect(response.body.user.email).toBe(userData.email);
expect(response.body.user).not.toHaveProperty('password');
});
it('should return 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
name: 'Test User',
password: 'Password123'
})
.expect(400);
expect(response.body).toHaveProperty('errors');
});
it('should return 409 for duplicate email', async () => {
const userData = {
email: 'duplicate@example.com',
name: 'Test User',
password: 'Password123'
};
// Create first user
await User.create(userData);
// Try to create duplicate
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(409);
expect(response.body.message).toMatch(/duplicate/i);
});
});
describe('GET /api/users/:id', () => {
it('should get user by ID', async () => {
const user = await User.create({
email: 'get@example.com',
name: 'Get User',
password: 'Password123'
});
const response = await request(app)
.get(`/api/users/${user._id}`)
.expect(200);
expect(response.body.user._id).toBe(user._id.toString());
});
it('should return 404 for non-existent user', async () => {
const fakeId = '507f1f77bcf86cd799439011';
await request(app)
.get(`/api/users/${fakeId}`)
.expect(404);
});
});
describe('PUT /api/users/:id', () => {
it('should update user', async () => {
const user = await User.create({
email: 'update@example.com',
name: 'Update User',
password: 'Password123'
});
const response = await request(app)
.put(`/api/users/${user._id}`)
.send({ name: 'Updated Name' })
.expect(200);
expect(response.body.user.name).toBe('Updated Name');
});
});
describe('DELETE /api/users/:id', () => {
it('should delete user', async () => {
const user = await User.create({
email: 'delete@example.com',
name: 'Delete User',
password: 'Password123'
});
await request(app)
.delete(`/api/users/${user._id}`)
.expect(204);
const deletedUser = await User.findById(user._id);
expect(deletedUser).toBeNull();
});
});
});javascript
// tests/integration/users.test.js
const request = require('supertest');
const app = require('../../src/app');
const User = require('../../src/models/User');
describe('User API', () => {
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'Password123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toHaveProperty('user');
expect(response.body.user.email).toBe(userData.email);
expect(response.body.user).not.toHaveProperty('password');
});
it('should return 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
name: 'Test User',
password: 'Password123'
})
.expect(400);
expect(response.body).toHaveProperty('errors');
});
it('should return 409 for duplicate email', async () => {
const userData = {
email: 'duplicate@example.com',
name: 'Test User',
password: 'Password123'
};
// 创建第一个用户
await User.create(userData);
// 尝试创建重复用户
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(409);
expect(response.body.message).toMatch(/duplicate/i);
});
});
describe('GET /api/users/:id', () => {
it('should get user by ID', async () => {
const user = await User.create({
email: 'get@example.com',
name: 'Get User',
password: 'Password123'
});
const response = await request(app)
.get(`/api/users/${user._id}`)
.expect(200);
expect(response.body.user._id).toBe(user._id.toString());
});
it('should return 404 for non-existent user', async () => {
const fakeId = '507f1f77bcf86cd799439011';
await request(app)
.get(`/api/users/${fakeId}`)
.expect(404);
});
});
describe('PUT /api/users/:id', () => {
it('should update user', async () => {
const user = await User.create({
email: 'update@example.com',
name: 'Update User',
password: 'Password123'
});
const response = await request(app)
.put(`/api/users/${user._id}`)
.send({ name: 'Updated Name' })
.expect(200);
expect(response.body.user.name).toBe('Updated Name');
});
});
describe('DELETE /api/users/:id', () => {
it('should delete user', async () => {
const user = await User.create({
email: 'delete@example.com',
name: 'Delete User',
password: 'Password123'
});
await request(app)
.delete(`/api/users/${user._id}`)
.expect(204);
const deletedUser = await User.findById(user._id);
expect(deletedUser).toBeNull();
});
});
});Authentication Testing
认证测试
javascript
// tests/integration/auth.test.js
const request = require('supertest');
const app = require('../../src/app');
const User = require('../../src/models/User');
describe('Authentication', () => {
let authToken;
let testUser;
beforeEach(async () => {
// Create test user
testUser = await User.create({
email: 'auth@example.com',
name: 'Auth User',
password: 'Password123'
});
// Login to get token
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'auth@example.com',
password: 'Password123'
});
authToken = response.body.token;
});
describe('POST /api/auth/login', () => {
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'auth@example.com',
password: 'Password123'
})
.expect(200);
expect(response.body).toHaveProperty('token');
expect(response.body).toHaveProperty('user');
});
it('should reject invalid credentials', async () => {
await request(app)
.post('/api/auth/login')
.send({
email: 'auth@example.com',
password: 'WrongPassword'
})
.expect(401);
});
});
describe('GET /api/auth/me', () => {
it('should get current user with valid token', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body.user.email).toBe('auth@example.com');
});
it('should reject request without token', async () => {
await request(app)
.get('/api/auth/me')
.expect(401);
});
it('should reject request with invalid token', async () => {
await request(app)
.get('/api/auth/me')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});javascript
// tests/integration/auth.test.js
const request = require('supertest');
const app = require('../../src/app');
const User = require('../../src/models/User');
describe('Authentication', () => {
let authToken;
let testUser;
beforeEach(async () => {
// 创建测试用户
testUser = await User.create({
email: 'auth@example.com',
name: 'Auth User',
password: 'Password123'
});
// 登录获取token
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'auth@example.com',
password: 'Password123'
});
authToken = response.body.token;
});
describe('POST /api/auth/login', () => {
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'auth@example.com',
password: 'Password123'
})
.expect(200);
expect(response.body).toHaveProperty('token');
expect(response.body).toHaveProperty('user');
});
it('should reject invalid credentials', async () => {
await request(app)
.post('/api/auth/login')
.send({
email: 'auth@example.com',
password: 'WrongPassword'
})
.expect(401);
});
});
describe('GET /api/auth/me', () => {
it('should get current user with valid token', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body.user.email).toBe('auth@example.com');
});
it('should reject request without token', async () => {
await request(app)
.get('/api/auth/me')
.expect(401);
});
it('should reject request with invalid token', async () => {
await request(app)
.get('/api/auth/me')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});Test Factories and Fixtures
测试工厂与测试数据
javascript
// tests/factories/userFactory.js
const User = require('../../src/models/User');
let userCount = 0;
exports.createUser = async (overrides = {}) => {
userCount++;
const defaultData = {
email: `user${userCount}@example.com`,
name: `User ${userCount}`,
password: 'Password123'
};
return User.create({ ...defaultData, ...overrides });
};
exports.createUsers = async (count, overrides = {}) => {
const users = [];
for (let i = 0; i < count; i++) {
users.push(await exports.createUser(overrides));
}
return users;
};Usage:
javascript
const { createUser, createUsers } = require('../factories/userFactory');
describe('User operations', () => {
it('should list all users', async () => {
await createUsers(5);
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body.users).toHaveLength(5);
});
it('should create admin user', async () => {
const admin = await createUser({ role: 'admin' });
expect(admin.role).toBe('admin');
});
});javascript
// tests/factories/userFactory.js
const User = require('../../src/models/User');
let userCount = 0;
exports.createUser = async (overrides = {}) => {
userCount++;
const defaultData = {
email: `user${userCount}@example.com`,
name: `User ${userCount}`,
password: 'Password123'
};
return User.create({ ...defaultData, ...overrides });
};
exports.createUsers = async (count, overrides = {}) => {
const users = [];
for (let i = 0; i < count; i++) {
users.push(await exports.createUser(overrides));
}
return users;
};使用方式:
javascript
const { createUser, createUsers } = require('../factories/userFactory');
describe('User operations', () => {
it('should list all users', async () => {
await createUsers(5);
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body.users).toHaveLength(5);
});
it('should create admin user', async () => {
const admin = await createUser({ role: 'admin' });
expect(admin.role).toBe('admin');
});
});Test Coverage
测试覆盖率
javascript
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest tests/unit",
"test:integration": "jest tests/integration"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"],
"collectCoverageFrom": [
"src/**/*.js",
"!src/tests/**"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}javascript
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest tests/unit",
"test:integration": "jest tests/integration"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"],
"collectCoverageFrom": [
"src/**/*.js",
"!src/tests/**"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}Production Operations
生产环境运维
Environment Configuration
环境配置
javascript
// config/index.js
require('dotenv').config();
const config = {
// Environment
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT, 10) || 3000,
// Database
database: {
url: process.env.DATABASE_URL,
poolMin: parseInt(process.env.DB_POOL_MIN, 10) || 2,
poolMax: parseInt(process.env.DB_POOL_MAX, 10) || 10
},
// Redis
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD
},
// JWT
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d'
},
// CORS
cors: {
origins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
},
// Rate Limiting
rateLimit: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 900000,
max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100
},
// Logging
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'logs/app.log'
}
};
// Validate required configuration
const requiredConfig = [
'database.url',
'jwt.secret'
];
requiredConfig.forEach(key => {
const value = key.split('.').reduce((obj, k) => obj?.[k], config);
if (!value) {
throw new Error(`Missing required configuration: ${key}`);
}
});
module.exports = config;.env.example:
bash
undefinedjavascript
// config/index.js
require('dotenv').config();
const config = {
// 环境
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT, 10) || 3000,
// 数据库
database: {
url: process.env.DATABASE_URL,
poolMin: parseInt(process.env.DB_POOL_MIN, 10) || 2,
poolMax: parseInt(process.env.DB_POOL_MAX, 10) || 10
},
// Redis
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD
},
// JWT
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d'
},
// CORS
cors: {
origins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
},
// 速率限制
rateLimit: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 900000,
max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100
},
// 日志
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'logs/app.log'
}
};
// 验证必填配置
const requiredConfig = [
'database.url',
'jwt.secret'
];
requiredConfig.forEach(key => {
const value = key.split('.').reduce((obj, k) => obj?.[k], config);
if (!value) {
throw new Error(`Missing required configuration: ${key}`);
}
});
module.exports = config;.env.example:
bash
undefinedEnvironment
环境
NODE_ENV=production
PORT=3000
NODE_ENV=production
PORT=3000
Database
数据库
DATABASE_URL=mongodb://localhost:27017/myapp
DB_POOL_MIN=2
DB_POOL_MAX=10
DATABASE_URL=mongodb://localhost:27017/myapp
DB_POOL_MIN=2
DB_POOL_MAX=10
Redis
Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
JWT
JWT
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
CORS
CORS
ALLOWED_ORIGINS=https://example.com,https://www.example.com
ALLOWED_ORIGINS=https://example.com,https://www.example.com
Rate Limiting
速率限制
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
Logging
日志
LOG_LEVEL=info
LOG_FILE=logs/app.log
undefinedLOG_LEVEL=info
LOG_FILE=logs/app.log
undefinedStructured Logging
结构化日志
javascript
// config/logger.js
const winston = require('winston');
const path = require('path');
const logLevels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4
};
const logColors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'blue'
};
winston.addColors(logColors);
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
const transports = [
// Error logs
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// Combined logs
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 5
})
];
// Console transport in development
if (process.env.NODE_ENV !== 'production') {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize({ all: true }),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`
)
)
})
);
}
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
levels: logLevels,
format,
transports
});
module.exports = logger;Usage:
javascript
const logger = require('./config/logger');
logger.info('Server started', { port: 3000 });
logger.error('Database connection failed', { error: err.message });
logger.debug('User data', { userId: user.id, email: user.email });Request Logging Middleware:
javascript
// middleware/requestLogger.js
const logger = require('../config/logger');
module.exports = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.http('Request completed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent'),
userId: req.user?.id
});
});
next();
};javascript
// config/logger.js
const winston = require('winston');
const path = require('path');
const logLevels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4
};
const logColors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'blue'
};
winston.addColors(logColors);
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
const transports = [
// 错误日志
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
})
];
// 开发环境添加控制台输出
if (process.env.NODE_ENV !== 'production') {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize({ all: true }),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`
)
)
})
);
}
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
levels: logLevels,
format,
transports
});
module.exports = logger;使用方式:
javascript
const logger = require('./config/logger');
logger.info('Server started', { port: 3000 });
logger.error('Database connection failed', { error: err.message });
logger.debug('User data', { userId: user.id, email: user.email });请求日志中间件:
javascript
// middleware/requestLogger.js
const logger = require('../config/logger');
module.exports = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.http('Request completed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent'),
userId: req.user?.id
});
});
next();
};Health Check Endpoints
健康检查端点
javascript
// routes/health.js
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const redis = require('redis');
const redisClient = redis.createClient();
// Basic health check
router.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
// Detailed health check
router.get('/health/detailed', async (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
services: {}
};
// Check MongoDB
try {
const mongoState = mongoose.connection.readyState;
health.services.mongodb = {
status: mongoState === 1 ? 'connected' : 'disconnected',
state: mongoState
};
} catch (error) {
health.services.mongodb = {
status: 'error',
error: error.message
};
health.status = 'degraded';
}
// Check Redis
try {
await redisClient.ping();
health.services.redis = {
status: 'connected'
};
} catch (error) {
health.services.redis = {
status: 'error',
error: error.message
};
health.status = 'degraded';
}
// Memory usage
const memUsage = process.memoryUsage();
health.memory = {
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`
};
const statusCode = health.status === 'ok' ? 200 : 503;
res.status(statusCode).json(health);
});
// Readiness check (Kubernetes)
router.get('/ready', async (req, res) => {
try {
// Check if app can serve requests
await mongoose.connection.db.admin().ping();
res.status(200).json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
// Liveness check (Kubernetes)
router.get('/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
module.exports = router;javascript
// routes/health.js
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const redis = require('redis');
const redisClient = redis.createClient();
// 基础健康检查
router.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
// 详细健康检查
router.get('/health/detailed', async (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
services: {}
};
// 检查MongoDB
try {
const mongoState = mongoose.connection.readyState;
health.services.mongodb = {
status: mongoState === 1 ? 'connected' : 'disconnected',
state: mongoState
};
} catch (error) {
health.services.mongodb = {
status: 'error',
error: error.message
};
health.status = 'degraded';
}
// 检查Redis
try {
await redisClient.ping();
health.services.redis = {
status: 'connected'
};
} catch (error) {
health.services.redis = {
status: 'error',
error: error.message
};
health.status = 'degraded';
}
// 内存使用情况
const memUsage = process.memoryUsage();
health.memory = {
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`
};
const statusCode = health.status === 'ok' ? 200 : 503;
res.status(statusCode).json(health);
});
// 就绪检查(Kubernetes)
router.get('/ready', async (req, res) => {
try {
// 检查应用是否可以处理请求
await mongoose.connection.db.admin().ping();
res.status(200).json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
// 存活检查(Kubernetes)
router.get('/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
module.exports = router;Graceful Shutdown
优雅停机
javascript
// server.js
const app = require('./app');
const logger = require('./config/logger');
const mongoose = require('./config/database');
const redis = require('./config/redis');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
// Graceful shutdown function
async function gracefulShutdown(signal) {
logger.info(`${signal} received, starting graceful shutdown`);
// Stop accepting new connections
server.close(async () => {
logger.info('HTTP server closed');
try {
// Close database connections
await mongoose.connection.close(false);
logger.info('MongoDB connection closed');
// Close Redis connection
await redis.quit();
logger.info('Redis connection closed');
// Close any other resources
// await closeOtherResources();
logger.info('Graceful shutdown completed');
process.exit(0);
} catch (error) {
logger.error('Error during shutdown', { error: error.message });
process.exit(1);
}
});
// Force shutdown after timeout
setTimeout(() => {
logger.error('Forcing shutdown after timeout');
process.exit(1);
}, 30000); // 30 seconds
}
// Handle termination signals
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// Handle uncaught errors
process.on('uncaughtException', (error) => {
logger.error('Uncaught exception', { error: error.message, stack: error.stack });
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled rejection', { reason, promise });
gracefulShutdown('unhandledRejection');
});
module.exports = server;javascript
// server.js
const app = require('./app');
const logger = require('./config/logger');
const mongoose = require('./config/database');
const redis = require('./config/redis');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
// 优雅停机函数
async function gracefulShutdown(signal) {
logger.info(`${signal} received, starting graceful shutdown`);
// 停止接受新连接
server.close(async () => {
logger.info('HTTP server closed');
try {
// 关闭数据库连接
await mongoose.connection.close(false);
logger.info('MongoDB connection closed');
// 关闭Redis连接
await redis.quit();
logger.info('Redis connection closed');
// 关闭其他资源
// await closeOtherResources();
logger.info('Graceful shutdown completed');
process.exit(0);
} catch (error) {
logger.error('Error during shutdown', { error: error.message });
process.exit(1);
}
});
// 超时后强制停机
setTimeout(() => {
logger.error('Forcing shutdown after timeout');
process.exit(1);
}, 30000); // 30秒
}
// 处理终止信号
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// 处理未捕获的错误
process.on('uncaughtException', (error) => {
logger.error('Uncaught exception', { error: error.message, stack: error.stack });
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled rejection', { reason, promise });
gracefulShutdown('unhandledRejection');
});
module.exports = server;PM2 Clustering
PM2集群模式
javascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'express-api',
script: './src/server.js',
// Clustering
instances: 'max', // Use all CPU cores
exec_mode: 'cluster',
// Environment variables
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080
},
// Restart policies
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
max_memory_restart: '500M',
// Graceful shutdown
kill_timeout: 5000,
wait_ready: true,
listen_timeout: 10000,
// Logging
error_file: './logs/pm2-error.log',
out_file: './logs/pm2-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
// Monitoring
instance_var: 'INSTANCE_ID',
// Watch (development only)
watch: false
}],
// Deploy configuration
deploy: {
production: {
user: 'deploy',
host: 'production.example.com',
ref: 'origin/main',
repo: 'git@github.com:username/repo.git',
path: '/var/www/production',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};PM2 Commands:
bash
undefinedjavascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'express-api',
script: './src/server.js',
// 集群配置
instances: 'max', // 使用所有CPU核心
exec_mode: 'cluster',
// 环境变量
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080
},
// 重启策略
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
max_memory_restart: '500M',
// 优雅停机
kill_timeout: 5000,
wait_ready: true,
listen_timeout: 10000,
// 日志
error_file: './logs/pm2-error.log',
out_file: './logs/pm2-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
// 监控
instance_var: 'INSTANCE_ID',
// 监听文件变化(仅开发环境)
watch: false
}],
// 部署配置
deploy: {
production: {
user: 'deploy',
host: 'production.example.com',
ref: 'origin/main',
repo: 'git@github.com:username/repo.git',
path: '/var/www/production',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};PM2命令:
bash
undefinedStart cluster
启动集群
pm2 start ecosystem.config.js --env production
pm2 start ecosystem.config.js --env production
Zero-downtime reload
零停机重载
pm2 reload express-api
pm2 reload express-api
Monitor
监控
pm2 monit
pm2 monit
View logs
查看日志
pm2 logs express-api
pm2 logs express-api
Scale instances
扩展实例数
pm2 scale express-api 4
pm2 scale express-api 4
Stop
停止
pm2 stop express-api
pm2 stop express-api
Restart
重启
pm2 restart express-api
pm2 restart express-api
Delete
删除
pm2 delete express-api
pm2 delete express-api
Save process list
保存进程列表
pm2 save
pm2 save
Startup script
设置开机自启
pm2 startup
pm2 startup
Deploy
部署
pm2 deploy production
undefinedpm2 deploy production
undefinedDevelopment Workflow
开发工作流
Nodemon Configuration
Nodemon配置
json
{
"watch": ["src"],
"ext": "js,json",
"ignore": [
"src/**/*.test.js",
"src/**/*.spec.js",
"node_modules/**/*",
"logs/**/*"
],
"exec": "node src/server.js",
"env": {
"NODE_ENV": "development",
"PORT": "3000"
},
"delay": 1000,
"verbose": false,
"restartable": "rs",
"signal": "SIGTERM"
}json
{
"watch": ["src"],
"ext": "js,json",
"ignore": [
"src/**/*.test.js",
"src/**/*.spec.js",
"node_modules/**/*",
"logs/**/*"
],
"exec": "node src/server.js",
"env": {
"NODE_ENV": "development",
"PORT": "3000"
},
"delay": 1000,
"verbose": false,
"restartable": "rs",
"signal": "SIGTERM"
}Package.json Scripts
Package.json脚本
json
{
"scripts": {
"dev": "nodemon src/server.js",
"dev:debug": "nodemon --inspect src/server.js",
"start": "node src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write \"src/**/*.js\"",
"prod": "pm2 start ecosystem.config.js --env production",
"reload": "pm2 reload express-api",
"stop": "pm2 stop express-api",
"logs": "pm2 logs express-api"
}
}json
{
"scripts": {
"dev": "nodemon src/server.js",
"dev:debug": "nodemon --inspect src/server.js",
"start": "node src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write \"src/**/*.js\"",
"prod": "pm2 start ecosystem.config.js --env production",
"reload": "pm2 reload express-api",
"stop": "pm2 stop express-api",
"logs": "pm2 logs express-api"
}
}Decision Trees
决策树
Middleware Selection
中间件选择
Need middleware?
├─ Security?
│ ├─ Headers → helmet
│ ├─ CORS → cors
│ ├─ Rate limiting → express-rate-limit
│ └─ Input validation → express-validator
├─ Parsing?
│ ├─ JSON → express.json()
│ ├─ Form data → express.urlencoded()
│ └─ Multipart → multer
├─ Logging?
│ ├─ Development → morgan('dev')
│ └─ Production → winston + morgan('combined')
├─ Compression?
│ └─ Response compression → compression()
└─ Authentication?
├─ Session-based → express-session + connect-redis
└─ Token-based → jsonwebtoken需要中间件?
├─ 安全相关?
│ ├─ 安全头 → helmet
│ ├─ 跨域 → cors
│ ├─ 速率限制 → express-rate-limit
│ └─ 输入验证 → express-validator
├─ 解析相关?
│ ├─ JSON → express.json()
│ ├─ 表单数据 → express.urlencoded()
│ └─ 多部分数据 → multer
├─ 日志相关?
│ ├─ 开发环境 → morgan('dev')
│ └─ 生产环境 → winston + morgan('combined')
├─ 压缩相关?
│ └─ 响应压缩 → compression()
└─ 认证相关?
├─ 会话式 → express-session + connect-redis
└─ 令牌式 → jsonwebtokenError Handling Strategy
错误处理策略
Error occurred?
├─ Operational error? (Known error)
│ ├─ Validation error → 400 with details
│ ├─ Authentication error → 401
│ ├─ Authorization error → 403
│ ├─ Not found error → 404
│ └─ Conflict error → 409
├─ Programming error? (Bug)
│ ├─ Development → Send full error + stack
│ └─ Production → Log error, send generic message
└─ External service error?
├─ Retry → Exponential backoff
└─ Circuit breaker → Fail fast发生错误?
├─ 业务错误?(已知错误)
│ ├─ 验证错误 → 400 + 详细信息
│ ├─ 认证错误 → 401
│ ├─ 授权错误 → 403
│ ├─ 资源不存在 → 404
│ └─ 资源冲突 → 409
├─ 编程错误?(Bug)
│ ├─ 开发环境 → 返回完整错误 + 堆栈
│ └─ 生产环境 → 记录错误,返回通用消息
└─ 外部服务错误?
├─ 重试 → 指数退避
└─ 熔断 → 快速失败Testing Approach
测试方法
What to test?
├─ API endpoints?
│ └─ Integration tests → Supertest
├─ Business logic?
│ └─ Unit tests → Jest
├─ Database operations?
│ └─ Integration tests → MongoMemoryServer
├─ Authentication?
│ └─ Integration tests → Test token flow
└─ Error handling?
└─ Unit + Integration tests → Test error cases需要测试什么?
├─ API端点?
│ └─ 集成测试 → Supertest
├─ 业务逻辑?
│ └─ 单元测试 → Jest
├─ 数据库操作?
│ └─ 集成测试 → MongoMemoryServer
├─ 认证流程?
│ └─ 集成测试 → 测试令牌流程
└─ 错误处理?
└─ 单元+集成测试 → 测试错误场景Deployment Pattern
部署模式
Deployment target?
├─ Local development?
│ └─ Nodemon
├─ Single server?
│ ├─ Small app → node server.js
│ └─ Production → PM2 (single instance)
├─ Multi-core server?
│ └─ PM2 cluster mode
├─ Container?
│ ├─ Single container → Docker + node
│ └─ Orchestrated → Docker + Kubernetes
└─ Serverless?
└─ AWS Lambda + API Gateway部署目标?
├─ 本地开发?
│ └─ Nodemon
├─ 单服务器?
│ ├─ 小型应用 → node server.js
│ └─ 生产环境 → PM2(单实例)
├─ 多核服务器?
│ └─ PM2集群模式
├─ 容器化?
│ ├─ 单容器 → Docker + node
│ └─ 编排部署 → Docker + Kubernetes
└─ 无服务器?
└─ AWS Lambda + API GatewayCommon Problems & Solutions
常见问题与解决方案
Problem 1: Port Already in Use
问题1:端口已被占用
Symptoms:
Error: listen EADDRINUSE: address already in use :::3000Solution:
bash
undefined症状:
Error: listen EADDRINUSE: address already in use :::3000解决方案:
bash
undefinedFind and kill process on port
查找并杀死占用端口的进程
lsof -ti:3000 | xargs kill -9
lsof -ti:3000 | xargs kill -9
Or use different port
或使用其他端口
PORT=3001 npm run dev
PORT=3001 npm run dev
Or add cleanup script
或添加清理脚本
{
"scripts": {
"predev": "kill-port 3000 || true",
"dev": "nodemon server.js"
}
}
undefined{
"scripts": {
"predev": "kill-port 3000 || true",
"dev": "nodemon server.js"
}
}
undefinedProblem 2: Middleware Order Issues
问题2:中间件顺序错误
Symptom: Routes not working, errors not caught, CORS failures
Solution: Follow correct middleware order:
- Security (helmet, cors)
- Rate limiting
- Parsing (json, urlencoded)
- Compression
- Logging
- Custom middleware
- Routes
- 404 handler
- Error handler (last!)
症状:路由无法工作、错误未被捕获、CORS失败
解决方案:遵循正确的中间件顺序:
- 安全中间件(helmet、cors)
- 速率限制
- 解析中间件(json、urlencoded)
- 压缩
- 日志
- 自定义中间件
- 路由
- 404处理器
- 错误处理器(最后!)
Problem 3: Unhandled Promise Rejections
问题3:未处理的Promise拒绝
Symptom:
UnhandledPromiseRejectionWarningSolution:
javascript
// Use catchAsync wrapper
const catchAsync = require('./utils/catchAsync');
app.get('/users', catchAsync(async (req, res) => {
const users = await User.find();
res.json({ users });
}));
// Or handle at process level
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION!', err);
server.close(() => process.exit(1));
});症状:
UnhandledPromiseRejectionWarning解决方案:
javascript
// 使用catchAsync包装器
const catchAsync = require('./utils/catchAsync');
app.get('/users', catchAsync(async (req, res) => {
const users = await User.find();
res.json({ users });
}));
// 或在进程级别处理
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION!', err);
server.close(() => process.exit(1));
});Problem 4: Sessions Not Working in Cluster Mode
问题4:集群模式下会话不工作
Symptom: User logged in but subsequent requests show logged out
Solution: Use Redis session store
javascript
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));症状:用户已登录,但后续请求显示未登录
解决方案:使用Redis会话存储
javascript
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));Problem 5: Memory Leaks
问题5:内存泄漏
Symptoms: Memory usage grows over time, server crashes
Solution:
bash
undefined症状:内存使用随时间增长,服务器崩溃
解决方案:
bash
undefinedMonitor memory with PM2
使用PM2监控内存
pm2 start server.js --max-memory-restart 500M
pm2 start server.js --max-memory-restart 500M
Profile with Node
使用Node.js调试
node --inspect server.js
node --inspect server.js
Then use Chrome DevTools
然后使用Chrome开发者工具
Use clinic.js
使用clinic.js
npm install -g clinic
clinic doctor -- node server.js
undefinednpm install -g clinic
clinic doctor -- node server.js
undefinedAnti-Patterns
反模式
❌ Don't: Mix Concerns
❌ 错误:混合关注点
javascript
// WRONG: Business logic in routes
app.post('/users', async (req, res) => {
const user = new User(req.body);
user.password = await bcrypt.hash(req.body.password, 10);
await user.save();
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET);
res.json({ user, token });
});✅ Do: Separate Concerns:
javascript
// CORRECT: Use controllers and services
app.post('/users',
validate(createUserRules),
userController.create
);
// controller
exports.create = catchAsync(async (req, res) => {
const user = await userService.createUser(req.body);
const token = authService.generateToken(user);
res.status(201).json({ user, token });
});javascript
// 错误:业务逻辑写在路由中
app.post('/users', async (req, res) => {
const user = new User(req.body);
user.password = await bcrypt.hash(req.body.password, 10);
await user.save();
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET);
res.json({ user, token });
});✅ 正确:分离关注点:
javascript
// 正确:使用控制器和服务
app.post('/users',
validate(createUserRules),
userController.create
);
// 控制器
exports.create = catchAsync(async (req, res) => {
const user = await userService.createUser(req.body);
const token = authService.generateToken(user);
res.status(201).json({ user, token });
});❌ Don't: Sync Operations
❌ 错误:同步操作
javascript
// WRONG
const data = fs.readFileSync('./data.json');✅ Do: Async Operations:
javascript
// CORRECT
const data = await fs.promises.readFile('./data.json');javascript
// 错误
const data = fs.readFileSync('./data.json');✅ 正确:异步操作:
javascript
// 正确
const data = await fs.promises.readFile('./data.json');❌ Don't: Trust User Input
❌ 错误:信任用户输入
javascript
// WRONG
app.post('/users', (req, res) => {
User.create(req.body); // Dangerous!
});✅ Do: Validate and Sanitize:
javascript
// CORRECT
app.post('/users',
validate(createUserRules),
userController.create
);javascript
// 错误
app.post('/users', (req, res) => {
User.create(req.body); // 危险!
});✅ 正确:验证与清理:
javascript
// 正确
app.post('/users',
validate(createUserRules),
userController.create
);Quick Reference
快速参考
Essential Middleware Stack
必备中间件栈
javascript
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const app = express();
// Minimal production stack
app.use(helmet());
app.use(cors());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(compression());
app.use(morgan('combined'));
// Routes
app.use('/api/v1', require('./routes'));
// Error handler
app.use(require('./middleware/errorHandler'));javascript
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const app = express();
// 最小生产环境栈
app.use(helmet());
app.use(cors());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(compression());
app.use(morgan('combined'));
// 路由
app.use('/api/v1', require('./routes'));
// 错误处理器
app.use(require('./middleware/errorHandler'));Essential Commands
必备命令
bash
undefinedbash
undefinedDevelopment
开发
npm run dev # Start with nodemon
npm test # Run tests
npm run test:watch # Watch mode
npm run lint # Lint code
npm run dev # 使用nodemon启动
npm test # 运行测试
npm run test:watch # 监听模式
npm run lint # 代码检查
Production
生产
npm start # Start production
pm2 start ecosystem.config.js # Start with PM2
pm2 reload app # Zero-downtime reload
pm2 logs app # View logs
pm2 monit # Monitor
npm start # 启动生产环境
pm2 start ecosystem.config.js # 使用PM2启动
pm2 reload app # 零停机重载
pm2 logs app # 查看日志
pm2 monit # 监控
Testing
测试
npm test # All tests
npm run test:unit # Unit tests
npm run test:integration # Integration tests
npm run test:coverage # Coverage report
undefinednpm test # 所有测试
npm run test:unit # 单元测试
npm run test:integration # 集成测试
npm run test:coverage # 覆盖率报告
undefinedRelated Skills
相关技能
- nodejs-backend - Node.js backend development patterns
- fastify-production - Fastify framework (performance-focused alternative)
- typescript-core - TypeScript with Express
- docker-containerization - Containerized Express deployment
- systematic-debugging - Advanced debugging techniques
- nodejs-backend - Node.js后端开发模式
- fastify-production - Fastify框架(性能优先的替代方案)
- typescript-core - TypeScript与Express结合使用
- docker-containerization - Express应用的容器化部署
- systematic-debugging - 高级调试技巧
Progressive Disclosure
进阶参考资料
For detailed implementation guides, see:
- Middleware Patterns - Advanced middleware composition and patterns
- Security Hardening - Comprehensive security checklist
- Testing Strategies - Complete testing guide
- Production Deployment - Deployment architectures and strategies
Version: Express 4.x, PM2 5.x, Node.js 18+
Last Updated: December 2025
License: MIT
如需详细实现指南,请查看:
- 中间件模式 - 高级中间件组合与模式
- 安全加固 - 全面安全检查清单
- 测试策略 - 完整测试指南
- 生产部署 - 部署架构与策略
版本: Express 4.x, PM2 5.x, Node.js 18+
最后更新: 2025年12月
许可证: MIT