| name | express-production |
| description | Production-ready Express.js development covering middleware architecture, error handling, security hardening, testing strategies, and deployment patterns |
| user-invocable | false |
| disable-model-invocation | true |
| skill_version | 2.0.0 |
| updated_at | "2025-12-03T00:00:00.000Z" |
| tags | ["express","nodejs","production","middleware","security","testing","deployment","backend"] |
| progressive_disclosure | {"entry_point":{"summary":"Production-ready Express.js framework with comprehensive middleware, security, testing, and deployment patterns","when_to_use":"Building production REST APIs, microservices, web applications requiring robust middleware architecture, security hardening, and comprehensive testing","quick_start":"1. npm install express 2. Create app.js with middleware stack 3. Implement error handling 4. Add security middleware 5. Deploy with PM2"},"references":["references/middleware-patterns.md","references/security-hardening.md","references/testing-strategies.md","references/production-deployment.md"]} |
| context_limit | 800 |
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:
npm install express
npm install express helmet cors express-rate-limit express-validator
npm install morgan winston compression
npm install dotenv
npm install -D nodemon supertest jest
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
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);
});
});
Run Development Server:
npm install -D nodemon
npx nodemon server.js
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();
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(compression());
if (process.env.NODE_ENV !== 'production') {
app.use(morgan('dev'));
} else {
app.use(morgan('combined'));
}
app.use(express.static('public'));
app.use(require('./middleware/requestId'));
app.use(require('./middleware/timing'));
app.use('/api/v1/users', require('./routes/users'));
app.use('/api/v1/posts', require('./routes/posts'));
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
app.use(require('./middleware/errorHandler'));
❌ Wrong Order:
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:
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:
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:
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');
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:
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 });
}));
❌ Wrong: No Error Handling:
app.get('/users', async (req, res) => {
const users = await User.find();
res.json({ users });
});
Middleware Composition
Compose Multiple Middleware:
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);
Conditional Middleware:
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
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:
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
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
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;
if (err.name === 'CastError') error = handleCastError(error);
if (err.code === 11000) error = handleDuplicateFields(error);
if (err.name === 'ValidationError') error = handleValidationError(error);
if (err.name === 'JsonWebTokenError') error = handleJWTError();
if (err.name === 'TokenExpiredError') error = handleJWTExpiredError();
sendErrorResponse(error, req, res);
};
Async Error Handling
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
const app = require('./app');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
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
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,
includeSubDomains: true,
preload: true
},
frameguard: {
action: 'deny'
},
noSniff: true,
xssFilter: true,
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
}
});
module.exports = securityConfig;
Usage:
const securityConfig = require('./config/security');
app.use(securityConfig);
CORS Configuration
const cors = require('cors');
const whitelist = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];
const corsOptions = {
origin: function (origin, callback) {
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
};
module.exports = cors(corsOptions);
Rate Limiting
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('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,
max: 100,
message: 'Too many requests from this IP, please try again later',
standardHeaders: true,
legacyHeaders: false
});
exports.authLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:auth:'
}),
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts, please try again later',
skipSuccessfulRequests: true
});
exports.apiKeyLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
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');
app.use('/api/', generalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
Input Validation and Sanitization
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(),
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
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) => {
});
const user = await User.findOne({ email: req.body.email });
const user = await db('users').where('email', req.body.email).first();
XSS Protection
const xss = require('xss-clean');
app.use(xss());
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
Environment Variable Security
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}`);
}
});
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
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
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
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'
});
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
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
{
"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
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: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d'
},
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:
NODE_ENV=production
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
DB_POOL_MIN=2
DB_POOL_MAX=10
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
ALLOWED_ORIGINS=https://example.com,https://www.example.com
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
LOG_LEVEL=info
LOG_FILE=logs/app.log
Structured Logging
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,
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;
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:
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
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: {}
};
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';
}
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);
});
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 });
}
});
router.get('/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
module.exports = router;
Graceful Shutdown
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');
await redis.quit();
logger.info('Redis connection closed');
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);
}
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
module.exports = {
apps: [{
name: 'express-api',
script: './src/server.js',
instances: 'max',
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 Commands:
pm2 start ecosystem.config.js --env production
pm2 reload express-api
pm2 monit
pm2 logs express-api
pm2 scale express-api 4
pm2 stop express-api
pm2 restart express-api
pm2 delete express-api
pm2 save
pm2 startup
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:
lsof -ti:3000 | xargs kill -9
PORT=3001 npm run dev
{
"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:
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
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:
pm2 start server.js --max-memory-restart 500M
node --inspect server.js
npm install -g clinic
clinic doctor -- node server.js
Anti-Patterns
❌ Don't: Mix Concerns
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:
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
const data = fs.readFileSync('./data.json');
✅ Do: Async Operations:
const data = await fs.promises.readFile('./data.json');
❌ Don't: Trust User Input
app.post('/users', (req, res) => {
User.create(req.body);
});
✅ Do: Validate and Sanitize:
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();
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
npm run dev
npm test
npm run test:watch
npm run lint
npm start
pm2 start ecosystem.config.js
pm2 reload app
pm2 logs app
pm2 monit
npm test
npm run test:unit
npm run test:integration
npm run test:coverage
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:
Version: Express 4.x, PM2 5.x, Node.js 18+
Last Updated: December 2025
License: MIT