express-production

安装量: 153
排名: #5642

安装

npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill express-production

Express.js - Production Web Framework 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:

Basic Express

npm install express

Production stack

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

Optional: Database and auth

npm install mongoose jsonwebtoken bcrypt

When 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 Quick Start Minimal Express Server // 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:

Install nodemon

npm install -D nodemon

Run with nodemon

npx nodemon server.js

Or add to package.json

npm run dev

Production-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.json

Middleware Architecture Understanding Middleware

Middleware functions are functions that have access to the request object (req), response object (res), and the next middleware function (next).

Middleware Types:

Application-level: app.use() or app.METHOD() Router-level: router.use() or router.METHOD() Error-handling: Four parameters (err, req, res, next) Built-in: express.json(), express.static() Third-party: helmet, cors, morgan Proper Middleware Order

✅ Correct Order:

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:

// 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!

Custom Middleware Patterns

Request ID Middleware:

// 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:

// 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:

// 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:

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 );

Async Middleware

✅ Correct Async Handling:

// 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:

// DON'T: Async without catch app.get('/users', async (req, res) => { const users = await User.find(); // Unhandled rejection! res.json({ users }); });

Middleware Composition

Compose Multiple Middleware:

// 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:

// 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') ));

Structured Error Handling Custom Error Classes // 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:

// 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 // 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;

Handling Specific Error Types // 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); };

Async Error Handling // 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 }); });

Unhandled Rejections // 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!'); }); });

Security Hardening Helmet.js Configuration // 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:

// app.js const securityConfig = require('./config/security');

app.use(securityConfig);

CORS Configuration // 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);

Rate Limiting // 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:

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);

Input Validation and Sanitization // 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);

SQL Injection Prevention // 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(); // Knex

XSS Protection // 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, "'"); };

Environment Variable Security // 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 } };

Testing with Supertest Test Setup // 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(); });

Integration Testing // 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();
});

}); });

Authentication Testing // 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);
});

}); });

Test Factories and Fixtures // 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:

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 // 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 // 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:

Environment

NODE_ENV=production PORT=3000

Database

DATABASE_URL=mongodb://localhost:27017/myapp DB_POOL_MIN=2 DB_POOL_MAX=10

Redis

REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=

JWT

JWT_SECRET=your-super-secret-jwt-key-min-32-chars JWT_EXPIRES_IN=7d JWT_REFRESH_EXPIRES_IN=30d

CORS

ALLOWED_ORIGINS=https://example.com,https://www.example.com

Rate Limiting

RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_MAX=100

Logging

LOG_LEVEL=info LOG_FILE=logs/app.log

Structured Logging // 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:

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:

// 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 // 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;

Graceful Shutdown // 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;

PM2 Clustering // 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:

Start cluster

pm2 start ecosystem.config.js --env production

Zero-downtime reload

pm2 reload express-api

Monitor

pm2 monit

View logs

pm2 logs express-api

Scale instances

pm2 scale express-api 4

Stop

pm2 stop express-api

Restart

pm2 restart express-api

Delete

pm2 delete express-api

Save process list

pm2 save

Startup script

pm2 startup

Deploy

pm2 deploy production

Development Workflow Nodemon Configuration { "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 { "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

Error 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

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

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

Common Problems & Solutions Problem 1: Port Already in Use

Symptoms:

Error: listen EADDRINUSE: address already in use :::3000

Solution:

Find and kill process on port

lsof -ti:3000 | xargs kill -9

Or use different port

PORT=3001 npm run dev

Or add cleanup script

{ "scripts": { "predev": "kill-port 3000 || true", "dev": "nodemon server.js" } }

Problem 2: Middleware Order Issues

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!) Problem 3: Unhandled Promise Rejections

Symptom: UnhandledPromiseRejectionWarning

Solution:

// 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)); });

Problem 4: Sessions Not Working in Cluster Mode

Symptom: User logged in but subsequent requests show logged out

Solution: Use Redis session store

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

Symptoms: Memory usage grows over time, server crashes

Solution:

Monitor memory with PM2

pm2 start server.js --max-memory-restart 500M

Profile with Node

node --inspect server.js

Then use Chrome DevTools

Use clinic.js

npm install -g clinic clinic doctor -- node server.js

Anti-Patterns ❌ Don't: Mix Concerns // 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:

// 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 }); });

❌ Don't: Sync Operations // WRONG const data = fs.readFileSync('./data.json');

✅ Do: Async Operations:

// CORRECT const data = await fs.promises.readFile('./data.json');

❌ Don't: Trust User Input // WRONG app.post('/users', (req, res) => { User.create(req.body); // Dangerous! });

✅ Do: Validate and Sanitize:

// CORRECT app.post('/users', validate(createUserRules), userController.create );

Quick Reference Essential Middleware Stack 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'));

Essential Commands

Development

npm run dev # Start with nodemon npm test # Run tests npm run test:watch # Watch mode npm run lint # Lint code

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

Testing

npm test # All tests npm run test:unit # Unit tests npm run test:integration # Integration tests npm run test:coverage # Coverage report

Related 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 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

返回排行榜