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