
Express Production
- 383 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
express-production is a Claude Code skill that hardens Express.js APIs with validation, middleware, security headers, structured errors, and operations-ready patterns for Node services heading to production.
About
express-production is a skill from bobmatnyc/claude-mpm-skills aimed at Node developers shipping Express APIs beyond prototype quality. The skill walks agents through input validation, middleware ordering, security headers, consistent error shapes, logging hooks, and deployment-minded defaults so services survive real traffic. Developers reach for express-production when an Express codebase works locally but lacks production guardrails—before cutover to staging or production environments.
- request validation
- security headers
- structured error handling
- middleware ordering
- local service verification
Express Production by the numbers
- 383 all-time installs (skills.sh)
- Ranked #1,134 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill express-productionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 383 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
How do you harden Express APIs for production?
Harden Express.js APIs with validation, middleware, security headers, structured errors, and ops-ready patterns for Node services heading to production.
Who is it for?
Node.js backend developers promoting an Express API from demo to production with security and observability gaps.
Skip if: Greenfield projects still choosing between Fastify, NestJS, or serverless handlers where Express patterns do not apply.
When should I use this skill?
User asks to productionize, secure, or harden an Express.js API or Node HTTP service.
What you get
Validated routes, security middleware stack, structured error handlers, and ops-ready Express server patterns.
- middleware config
- error handler module
- security header setup
Files
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 bcryptWhen 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 devProduction-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.jsonMiddleware 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: 1. Application-level: app.use() or app.METHOD() 2. Router-level: router.use() or router.METHOD() 3. Error-handling: Four parameters (err, req, res, next) 4. Built-in: express.json(), express.static() 5. 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(); // KnexXSS 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, """)
.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.logStructured 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 productionDevelopment 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 → jsonwebtokenError Handling Strategy
Error occurred?
├─ Operational error? (Known error)
│ ├─ Validation error → 400 with details
│ ├─ Authentication error → 401
│ ├─ Authorization error → 403
│ ├─ Not found error → 404
│ └─ Conflict error → 409
├─ Programming error? (Bug)
│ ├─ Development → Send full error + stack
│ └─ Production → Log error, send generic message
└─ External service error?
├─ Retry → Exponential backoff
└─ Circuit breaker → Fail fastTesting 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 casesDeployment 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 GatewayCommon Problems & Solutions
Problem 1: Port Already in Use
Symptoms:
Error: listen EADDRINUSE: address already in use :::3000Solution:
# 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: 1. Security (helmet, cors) 2. Rate limiting 3. Parsing (json, urlencoded) 4. Compression 5. Logging 6. Custom middleware 7. Routes 8. 404 handler 9. 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.jsAnti-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 reportRelated 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
{
"name": "express-production",
"version": "2.0.0",
"category": "toolchain",
"toolchain": "javascript",
"framework": "express",
"tags": [
"express",
"nodejs",
"production",
"middleware",
"security",
"testing",
"deployment",
"backend",
"rest-api",
"microservices",
"authentication",
"error-handling"
],
"entry_point_tokens": 120,
"full_tokens": 27280,
"related_skills": [
"nodejs-backend",
"typescript-core",
"docker-containerization",
"systematic-debugging",
"fastify-production",
"testing-anti-patterns",
"verification-before-completion"
],
"author": "Claude MPM Team",
"license": "MIT",
"requires": [],
"updated": "2025-12-03",
"source_path": "SKILL.md",
"source": "https://github.com/bobmatnyc/claude-mpm",
"created": "2025-11-21",
"modified": "2025-12-03",
"maintainer": "Claude MPM Team",
"attribution_required": true,
"repository": "https://github.com/bobmatnyc/claude-mpm-skills",
"progressive_disclosure": {
"enabled": true,
"references": [
"references/middleware-patterns.md",
"references/security-hardening.md",
"references/testing-strategies.md",
"references/production-deployment.md"
]
},
"skill_level": "production",
"complexity": "comprehensive",
"coverage": [
"middleware-architecture",
"error-handling",
"security-hardening",
"testing-strategies",
"production-deployment",
"pm2-clustering",
"logging-monitoring",
"graceful-shutdown"
]
}
Advanced Middleware Patterns
Progressive disclosure reference for Express middleware architecture
Middleware Composition Patterns
Sequential Composition
Execute middleware in strict order:
const sequence = (...middleware) => {
return async (req, res, next) => {
let index = 0;
const runNext = async () => {
if (index >= middleware.length) return next();
const current = middleware[index++];
await current(req, res, runNext);
};
try {
await runNext();
} catch (error) {
next(error);
}
};
};
// Usage
app.use('/admin', sequence(
authenticate,
requireAdmin,
auditLog,
rateLimit
));Conditional Middleware
const when = (condition, middleware) => {
return (req, res, next) => {
if (condition(req, res)) {
return middleware(req, res, next);
}
next();
};
};
const unless = (condition, middleware) => {
return when((req) => !condition(req), middleware);
};
// Usage
app.use(when(
(req) => req.path.startsWith('/api'),
rateLimit
));
app.use(unless(
(req) => req.path === '/health',
authenticate
));Parallel Middleware
Execute independent middleware concurrently:
const parallel = (...middleware) => {
return async (req, res, next) => {
try {
await Promise.all(
middleware.map(fn =>
new Promise((resolve, reject) => {
fn(req, res, (err) => {
if (err) reject(err);
else resolve();
});
})
)
);
next();
} catch (error) {
next(error);
}
};
};
// Usage - load user and permissions in parallel
app.use('/dashboard', parallel(
loadUser,
loadPermissions,
loadSettings
));Advanced Authentication Patterns
Multi-Strategy Authentication
// middleware/multiAuth.js
const strategies = {
jwt: (req) => {
const token = req.headers.authorization?.split(' ')[1];
return verifyJWT(token);
},
apiKey: (req) => {
const key = req.headers['x-api-key'];
return verifyApiKey(key);
},
session: (req) => {
return req.session?.user;
}
};
exports.authenticate = (allowedStrategies = ['jwt']) => {
return async (req, res, next) => {
for (const strategy of allowedStrategies) {
try {
const user = await strategies[strategy](req);
if (user) {
req.user = user;
req.authStrategy = strategy;
return next();
}
} catch (error) {
// Try next strategy
continue;
}
}
next(new AuthenticationError('Authentication required'));
};
};
// Usage
app.get('/api/data',
authenticate(['jwt', 'apiKey']),
getData
);Permission-Based Authorization
// middleware/permissions.js
const permissions = {
'users:read': ['admin', 'user', 'guest'],
'users:write': ['admin', 'user'],
'users:delete': ['admin'],
'posts:publish': ['admin', 'editor']
};
exports.can = (permission) => {
return (req, res, next) => {
if (!req.user) {
return next(new AuthenticationError());
}
const allowedRoles = permissions[permission];
if (!allowedRoles || !allowedRoles.includes(req.user.role)) {
return next(new AuthorizationError(
`Permission denied: ${permission}`
));
}
next();
};
};
// Usage
app.delete('/users/:id',
authenticate,
can('users:delete'),
deleteUser
);Request Context and Tracing
Request Context Middleware
// middleware/context.js
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
exports.contextMiddleware = (req, res, next) => {
const context = {
requestId: req.id,
userId: req.user?.id,
timestamp: Date.now(),
method: req.method,
path: req.path
};
asyncLocalStorage.run(context, () => {
next();
});
};
exports.getContext = () => {
return asyncLocalStorage.getStore();
};
// Usage in any module
const { getContext } = require('./middleware/context');
function someFunction() {
const ctx = getContext();
logger.info('Processing request', { requestId: ctx.requestId });
}Distributed Tracing
// middleware/tracing.js
const opentelemetry = require('@opentelemetry/api');
exports.tracingMiddleware = (req, res, next) => {
const tracer = opentelemetry.trace.getTracer('express-app');
const span = tracer.startSpan(`${req.method} ${req.path}`, {
attributes: {
'http.method': req.method,
'http.url': req.url,
'http.target': req.path,
'http.user_agent': req.get('user-agent')
}
});
req.span = span;
res.on('finish', () => {
span.setAttribute('http.status_code', res.statusCode);
span.end();
});
next();
};Caching Middleware
Response Caching
// middleware/cache.js
const redis = require('redis');
const client = redis.createClient();
exports.cacheMiddleware = (duration = 300) => {
return async (req, res, next) => {
// Only cache GET requests
if (req.method !== 'GET') {
return next();
}
const key = `cache:${req.url}`;
try {
const cached = await client.get(key);
if (cached) {
res.setHeader('X-Cache', 'HIT');
return res.json(JSON.parse(cached));
}
} catch (error) {
// Cache miss, continue
}
// Capture original json method
const originalJson = res.json.bind(res);
res.json = (data) => {
// Cache the response
client.setEx(key, duration, JSON.stringify(data))
.catch(err => console.error('Cache error:', err));
res.setHeader('X-Cache', 'MISS');
return originalJson(data);
};
next();
};
};
// Usage
app.get('/api/posts',
cacheMiddleware(600), // 10 minutes
getPosts
);Conditional Caching
// middleware/conditionalCache.js
exports.conditionalCache = (options = {}) => {
const {
duration = 300,
condition = () => true,
keyGenerator = (req) => req.url
} = options;
return async (req, res, next) => {
if (!condition(req, res)) {
return next();
}
const key = `cache:${keyGenerator(req)}`;
// Check cache
const cached = await redis.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// Intercept response
const originalJson = res.json.bind(res);
res.json = (data) => {
redis.setEx(key, duration, JSON.stringify(data));
return originalJson(data);
};
next();
};
};
// Usage
app.get('/api/users',
conditionalCache({
duration: 300,
condition: (req) => !req.user?.isAdmin,
keyGenerator: (req) => `users:${req.query.page || 1}`
}),
getUsers
);Request Transformation
Body Transformation Middleware
// middleware/transform.js
exports.transformRequest = (transformer) => {
return (req, res, next) => {
if (req.body) {
req.body = transformer(req.body);
}
next();
};
};
exports.transformResponse = (transformer) => {
return (req, res, next) => {
const originalJson = res.json.bind(res);
res.json = (data) => {
const transformed = transformer(data);
return originalJson(transformed);
};
next();
};
};
// Usage
const camelCaseKeys = require('camelcase-keys');
const snakeCaseKeys = require('snakecase-keys');
// Transform incoming snake_case to camelCase
app.use(transformRequest(camelCaseKeys));
// Transform outgoing camelCase to snake_case for API consumers
app.use(transformResponse(snakeCaseKeys));Error Handling Middleware
Typed Error Handler
// middleware/typedErrorHandler.js
const errorHandlers = new Map();
errorHandlers.set('ValidationError', (err, req, res) => {
res.status(400).json({
error: 'Validation failed',
details: err.details
});
});
errorHandlers.set('UnauthorizedError', (err, req, res) => {
res.status(401).json({
error: 'Authentication required',
message: err.message
});
});
errorHandlers.set('ForbiddenError', (err, req, res) => {
res.status(403).json({
error: 'Access denied',
message: err.message
});
});
errorHandlers.set('NotFoundError', (err, req, res) => {
res.status(404).json({
error: 'Resource not found',
resource: err.resource
});
});
module.exports = (err, req, res, next) => {
const handler = errorHandlers.get(err.name);
if (handler) {
return handler(err, req, res);
}
// Default error handler
console.error(err);
res.status(500).json({
error: 'Internal server error'
});
};Performance Middleware
Request Throttling
// middleware/throttle.js
const bottleneck = require('bottleneck');
exports.throttle = (options = {}) => {
const limiter = new bottleneck({
maxConcurrent: options.maxConcurrent || 10,
minTime: options.minTime || 100
});
return (req, res, next) => {
limiter.schedule(() => {
return new Promise((resolve) => {
res.on('finish', resolve);
next();
});
});
};
};
// Usage - limit concurrent database queries
app.use('/api/heavy-query',
throttle({ maxConcurrent: 5, minTime: 200 }),
heavyQueryHandler
);Request Timeout
// middleware/timeout.js
exports.timeout = (ms = 30000) => {
return (req, res, next) => {
const timer = setTimeout(() => {
if (!res.headersSent) {
res.status(503).json({
error: 'Request timeout',
timeout: ms
});
}
}, ms);
res.on('finish', () => clearTimeout(timer));
next();
};
};
// Usage
app.use('/api/slow-endpoint',
timeout(5000), // 5 second timeout
slowHandler
);Related Patterns
- Security Hardening - Security-focused middleware
- Testing Strategies - Testing middleware
- Production Deployment - Production middleware patterns
Production Deployment Strategies
Comprehensive deployment guide for Express applications
Deployment Architecture Patterns
Single Server Deployment
Use Case: Small applications, MVP, development staging
┌─────────────────────────────────────┐
│ Load Balancer / NGINX │
│ (SSL Termination) │
└─────────────┬───────────────────────┘
│
┌─────────▼─────────┐
│ PM2 Cluster │
│ (4 instances) │
├───────────────────┤
│ Express App │
│ + MongoDB │
│ + Redis │
└───────────────────┘Implementation:
# Install PM2
npm install -g pm2
# Start cluster
pm2 start ecosystem.config.js --env production
# Save PM2 config
pm2 save
# Auto-start on boot
pm2 startupMulti-Server Deployment
Use Case: Medium-high traffic, high availability required
┌────────────────┐
│ Load Balancer │
│ (HAProxy) │
└───────┬────────┘
│
┌───────────┴───────────┐
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ Server 1 │ │ Server 2 │
│ PM2 Cluster│ │ PM2 Cluster│
│(4 instances)│ │(4 instances)│
└─────┬──────┘ └─────┬──────┘
│ │
└───────────┬───────────┘
│
┌────────────▼────────────┐
│ Shared Services │
├─────────────────────────┤
│ PostgreSQL (Primary) │
│ PostgreSQL (Replica) │
│ Redis (Sentinel) │
└─────────────────────────┘Microservices Architecture
Use Case: Large applications, team scalability
┌──────────────┐
│ API Gateway │
│ (Kong) │
└──────┬───────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
┌────▼─────┐ ┌───────▼──────┐ ┌───────▼──────┐
│ Auth │ │ Users │ │ Posts │
│ Service │ │ Service │ │ Service │
│ (Express)│ │ (Express) │ │ (Express) │
└──────────┘ └──────────────┘ └──────────────┘
│ │ │
└───────────────────┼─────────────────────┘
│
┌──────────▼───────────┐
│ Message Queue │
│ (RabbitMQ/Redis) │
└──────────────────────┘Container Deployment
Docker Configuration
Multi-Stage Dockerfile:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source
COPY . .
# Production stage
FROM node:18-alpine
# Security: Run as non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy from builder
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# Start application
CMD ["node", "src/server.js"]healthcheck.js:
const http = require('http');
const options = {
host: 'localhost',
port: process.env.PORT || 3000,
path: '/health',
timeout: 2000
};
const healthCheck = http.request(options, (res) => {
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});
healthCheck.on('error', () => {
process.exit(1);
});
healthCheck.end();Docker Compose (Development/Staging)
version: '3.8'
services:
app:
build:
context: .
target: builder
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
volumes:
- ./logs:/app/logs
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:Kubernetes Deployment
Deployment Manifest
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: express-app
labels:
app: express-app
spec:
replicas: 3
selector:
matchLabels:
app: express-app
template:
metadata:
labels:
app: express-app
spec:
containers:
- name: express-app
image: myregistry/express-app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: REDIS_URL
valueFrom:
configMapKeyRef:
name: app-config
key: redis-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: express-app-service
spec:
selector:
app: express-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer
---
# hpa.yaml (Horizontal Pod Autoscaler)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: express-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: express-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80ConfigMap and Secrets
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
redis-url: "redis://redis-service:6379"
log-level: "info"
---
# secrets.yaml (base64 encoded)
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
database-url: <base64-encoded-url>
jwt-secret: <base64-encoded-secret>CI/CD Pipeline
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Run security audit
run: npm audit --production
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ secrets.DOCKER_REGISTRY }}/express-app:latest
${{ secrets.DOCKER_REGISTRY }}/express-app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: |
k8s/deployment.yaml
k8s/service.yaml
k8s/hpa.yaml
images: |
${{ secrets.DOCKER_REGISTRY }}/express-app:${{ github.sha }}
kubectl-version: 'latest'GitLab CI
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
test:
stage: test
image: node:18-alpine
before_script:
- npm ci
script:
- npm run lint
- npm test
- npm audit --production
cache:
paths:
- node_modules/
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/express-app express-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- kubectl rollout status deployment/express-app
only:
- mainBlue-Green Deployment
Strategy
# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: express-app-blue
spec:
replicas: 3
selector:
matchLabels:
app: express-app
version: blue
template:
metadata:
labels:
app: express-app
version: blue
spec:
containers:
- name: express-app
image: myregistry/express-app:v1.0.0
---
# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: express-app-green
spec:
replicas: 3
selector:
matchLabels:
app: express-app
version: green
template:
metadata:
labels:
app: express-app
version: green
spec:
containers:
- name: express-app
image: myregistry/express-app:v2.0.0
---
# service.yaml (switch by changing selector)
apiVersion: v1
kind: Service
metadata:
name: express-app-service
spec:
selector:
app: express-app
version: blue # Change to 'green' for deployment
ports:
- protocol: TCP
port: 80
targetPort: 3000Deployment Script
#!/bin/bash
# deploy-blue-green.sh
CURRENT_COLOR=$(kubectl get svc express-app-service -o jsonpath='{.spec.selector.version}')
NEW_COLOR="green"
if [ "$CURRENT_COLOR" = "green" ]; then
NEW_COLOR="blue"
fi
echo "Current: $CURRENT_COLOR, Deploying: $NEW_COLOR"
# Deploy new version
kubectl apply -f ${NEW_COLOR}-deployment.yaml
# Wait for deployment
kubectl rollout status deployment/express-app-${NEW_COLOR}
# Run smoke tests
./smoke-tests.sh http://express-app-${NEW_COLOR}:3000
if [ $? -eq 0 ]; then
echo "Smoke tests passed, switching traffic"
# Switch service to new version
kubectl patch svc express-app-service -p "{\"spec\":{\"selector\":{\"version\":\"${NEW_COLOR}\"}}}"
echo "Deployment complete. Old version ($CURRENT_COLOR) still running for rollback."
else
echo "Smoke tests failed, keeping current version"
exit 1
fiMonitoring and Observability
Prometheus Metrics
// middleware/metrics.js
const promClient = require('prom-client');
// Register default metrics
const register = new promClient.Registry();
promClient.collectDefaultMetrics({ register });
// Custom metrics
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
registers: [register]
});
const httpRequestTotal = new promClient.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
registers: [register]
});
exports.metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route?.path || req.path;
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration);
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
});
next();
};
// Metrics endpoint
exports.metricsEndpoint = async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
};Application Performance Monitoring (APM)
// apm.js (New Relic example)
require('newrelic');
// Or Datadog
const tracer = require('dd-trace').init({
service: 'express-app',
env: process.env.NODE_ENV
});
// Or Elastic APM
const apm = require('elastic-apm-node').start({
serviceName: 'express-app',
serverUrl: process.env.APM_SERVER_URL
});Zero-Downtime Deployment Checklist
- [ ] Implement health checks (liveness + readiness)
- [ ] Configure graceful shutdown (SIGTERM handling)
- [ ] Use rolling updates (not recreate)
- [ ] Set appropriate readiness probe delays
- [ ] Implement database migration strategy
- [ ] Use feature flags for breaking changes
- [ ] Configure proper resource requests/limits
- [ ] Test rollback procedure
- [ ] Monitor metrics during deployment
- [ ] Have rollback plan ready
Database Migration Strategy
Migrate-then-Deploy
// migrations/20250101_add_user_fields.js
exports.up = async (db) => {
// Add new column (backward compatible)
await db.schema.table('users', (table) => {
table.string('phone_number').nullable();
});
};
exports.down = async (db) => {
await db.schema.table('users', (table) => {
table.dropColumn('phone_number');
});
};Deployment Process: 1. Run migration (add nullable column) 2. Deploy new code (uses new column) 3. Backfill data 4. Make column non-nullable (if needed)
Pre-Deployment Script
#!/bin/bash
# pre-deploy.sh
# Run migrations
npm run migrate:up
# Verify migration
if [ $? -ne 0 ]; then
echo "Migration failed"
exit 1
fi
# Continue with deploymentPerformance Optimization
NGINX Configuration
# /etc/nginx/nginx.conf
upstream express_app {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3002 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3003 max_fails=3 fail_timeout=30s;
keepalive 64;
}
server {
listen 80;
server_name example.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL Configuration
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Static files
location /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy
location / {
proxy_pass http://express_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}Disaster Recovery
Backup Strategy
#!/bin/bash
# backup.sh
# Database backup
pg_dump -U user -h localhost myapp > backup_$(date +%Y%m%d_%H%M%S).sql
# Upload to S3
aws s3 cp backup_*.sql s3://mybucket/backups/
# Retain only last 30 days
find /backups -name "backup_*.sql" -mtime +30 -deleteRestore Procedure
#!/bin/bash
# restore.sh
# Download from S3
aws s3 cp s3://mybucket/backups/backup_20250101_120000.sql .
# Restore database
psql -U user -h localhost myapp < backup_20250101_120000.sqlRelated Resources
Security Hardening Checklist
Comprehensive security guide for production Express applications
Security Headers
Complete Helmet Configuration
// config/security.js
const helmet = require('helmet');
const securityHeaders = helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "api.example.com"],
fontSrc: ["'self'", "fonts.gstatic.com"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
sandbox: ['allow-forms', 'allow-scripts'],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: []
}
},
// HTTP Strict Transport Security
hsts: {
maxAge: 31536000, // 1 year in seconds
includeSubDomains: true,
preload: true
},
// Prevent clickjacking
frameguard: {
action: 'deny'
},
// Prevent MIME type sniffing
noSniff: true,
// Enable XSS filter
xssFilter: true,
// Hide X-Powered-By header
hidePoweredBy: true,
// Referrer Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
},
// Expect-CT header
expectCt: {
maxAge: 86400,
enforce: true
}
});
module.exports = securityHeaders;Custom Security Headers
// middleware/securityHeaders.js
module.exports = (req, res, next) => {
// Permissions Policy (formerly Feature Policy)
res.setHeader('Permissions-Policy',
'geolocation=(), microphone=(), camera=()'
);
// Prevent information disclosure
res.removeHeader('X-Powered-By');
res.setHeader('X-Content-Type-Options', 'nosniff');
// Cache control for sensitive data
if (req.path.startsWith('/api/')) {
res.setHeader('Cache-Control', 'no-store, max-age=0');
}
next();
};Input Validation and Sanitization
Comprehensive Validation Rules
// validators/user.js
const { body, param, query } = require('express-validator');
// Email validation with security checks
exports.emailRules = body('email')
.trim()
.isEmail()
.normalizeEmail()
.custom(async (email) => {
// Check for disposable email domains
const disposableDomains = ['tempmail.com', 'throwaway.email'];
const domain = email.split('@')[1];
if (disposableDomains.includes(domain)) {
throw new Error('Disposable email addresses not allowed');
}
return true;
});
// Strong password requirements
exports.passwordRules = body('password')
.isLength({ min: 12, max: 128 })
.withMessage('Password must be 12-128 characters')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.withMessage('Password must include: uppercase, lowercase, number, special char')
.custom((value, { req }) => {
// Prevent common passwords
const commonPasswords = ['Password123!', 'Admin123!'];
if (commonPasswords.includes(value)) {
throw new Error('Password too common');
}
return true;
});
// SQL injection prevention in search
exports.searchRules = query('q')
.trim()
.isLength({ max: 200 })
.matches(/^[a-zA-Z0-9\s-]*$/)
.withMessage('Invalid search characters');
// File upload validation
exports.fileUploadRules = body('file')
.custom((value, { req }) => {
if (!req.file) {
throw new Error('File required');
}
// Check file type
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedMimes.includes(req.file.mimetype)) {
throw new Error('Invalid file type');
}
// Check file size (5MB max)
if (req.file.size > 5 * 1024 * 1024) {
throw new Error('File too large');
}
return true;
});XSS Prevention
// middleware/xssProtection.js
const xss = require('xss-clean');
const validator = require('validator');
// Apply XSS cleaning middleware
app.use(xss());
// Custom HTML escaping
const escapeHtml = (unsafe) => {
return validator.escape(unsafe);
};
// Sanitize user-generated content
const sanitizeUserContent = (content) => {
const xssOptions = {
whiteList: {
// Only allow specific safe tags
p: [],
br: [],
strong: [],
em: [],
a: ['href', 'title']
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
};
return xss(content, xssOptions);
};
module.exports = { escapeHtml, sanitizeUserContent };Authentication Security
Secure Password Hashing
// utils/password.js
const bcrypt = require('bcrypt');
const crypto = require('crypto');
// Use high cost factor for bcrypt
const SALT_ROUNDS = 12;
exports.hashPassword = async (password) => {
return bcrypt.hash(password, SALT_ROUNDS);
};
exports.verifyPassword = async (password, hash) => {
return bcrypt.compare(password, hash);
};
// Constant-time comparison to prevent timing attacks
exports.secureCompare = (a, b) => {
return crypto.timingSafeEqual(
Buffer.from(a),
Buffer.from(b)
);
};JWT Security Best Practices
// utils/jwt.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Generate strong secret (run once, store in env)
const generateSecret = () => {
return crypto.randomBytes(64).toString('hex');
};
// Secure JWT configuration
exports.signToken = (payload) => {
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '15m', // Short-lived access tokens
algorithm: 'HS256',
issuer: 'your-app',
audience: 'your-api'
});
};
exports.signRefreshToken = (payload) => {
return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, {
expiresIn: '7d',
algorithm: 'HS256',
issuer: 'your-app',
audience: 'your-api'
});
};
exports.verifyToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'your-app',
audience: 'your-api'
});
} catch (error) {
throw new Error('Invalid token');
}
};
// Token rotation for refresh tokens
exports.rotateRefreshToken = async (oldToken) => {
const decoded = jwt.verify(oldToken, process.env.JWT_REFRESH_SECRET);
// Blacklist old token
await redis.setEx(`blacklist:${oldToken}`, 7 * 24 * 60 * 60, '1');
// Issue new refresh token
return exports.signRefreshToken({ userId: decoded.userId });
};Rate Limiting Strategies
// middleware/advancedRateLimit.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const redisClient = redis.createClient();
// Tier-based rate limiting
exports.apiLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000,
max: async (req) => {
// Different limits based on user tier
if (req.user?.tier === 'premium') return 1000;
if (req.user?.tier === 'standard') return 500;
return 100; // Free tier
},
keyGenerator: (req) => {
return req.user?.id || req.ip;
},
skip: (req) => {
// Skip for internal IPs
const internalIPs = ['127.0.0.1', '::1'];
return internalIPs.includes(req.ip);
}
});
// Exponential backoff for failed auth
exports.authLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
onLimitReached: async (req) => {
// Log potential brute force attack
logger.warn('Rate limit reached', {
ip: req.ip,
endpoint: req.path
});
// Temporarily ban IP after multiple limit hits
const key = `ban:${req.ip}`;
const banCount = await redis.incr(key);
if (banCount > 3) {
await redis.setEx(key, 60 * 60, banCount); // 1 hour ban
}
}
});
// Distributed DoS protection
exports.ddosProtection = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 60,
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
});
}
});Session Security
Secure Session Configuration
// config/session.js
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
const sessionConfig = session({
store: new RedisStore({
client: redisClient,
prefix: 'sess:',
ttl: 60 * 60 * 24 // 24 hours
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
rolling: true, // Reset expiration on activity
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // No client-side JS access
maxAge: 1000 * 60 * 60 * 24, // 24 hours
sameSite: 'strict', // CSRF protection
domain: process.env.COOKIE_DOMAIN
},
name: 'sessionId' // Don't use default 'connect.sid'
});
module.exports = sessionConfig;CSRF Protection
// middleware/csrf.js
const csrf = require('csurf');
const crypto = require('crypto');
// Standard CSRF protection
exports.csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
// Double submit cookie pattern
exports.doubleSubmitCsrf = (req, res, next) => {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
// Generate token for safe methods
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
httpOnly: false, // Client needs to read this
secure: true,
sameSite: 'strict'
});
return next();
}
// Verify token for unsafe methods
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
next();
};SQL and NoSQL Injection Prevention
// Always use parameterized queries or ORMs
// ❌ WRONG - SQL Injection vulnerable
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;
// ✅ CORRECT - Parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [req.body.email]);
// ✅ CORRECT - ORM (Sequelize)
const user = await User.findOne({
where: { email: req.body.email }
});
// ❌ WRONG - NoSQL Injection vulnerable
const user = await User.findOne({ email: req.body.email });
// If req.body.email = { $gt: "" }, returns all users!
// ✅ CORRECT - Validate and sanitize
const { body } = require('express-validator');
const emailValidator = body('email')
.isEmail()
.normalizeEmail()
.trim();
// ✅ CORRECT - Explicitly reject objects
const sanitizeInput = (input) => {
if (typeof input === 'object' && input !== null) {
throw new Error('Invalid input type');
}
return input;
};File Upload Security
// middleware/secureUpload.js
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const sharp = require('sharp');
// Secure file storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
// Use random filename to prevent path traversal
const randomName = crypto.randomBytes(16).toString('hex');
const ext = path.extname(file.originalname);
cb(null, `${randomName}${ext}`);
}
});
// File filter with security checks
const fileFilter = (req, file, cb) => {
// Whitelist allowed MIME types
const allowedMimes = [
'image/jpeg',
'image/png',
'image/webp',
'application/pdf'
];
if (!allowedMimes.includes(file.mimetype)) {
return cb(new Error('Invalid file type'), false);
}
// Check file extension
const ext = path.extname(file.originalname).toLowerCase();
const allowedExts = ['.jpg', '.jpeg', '.png', '.webp', '.pdf'];
if (!allowedExts.includes(ext)) {
return cb(new Error('Invalid file extension'), false);
}
cb(null, true);
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB max
files: 1 // Single file upload
}
});
// Image validation and sanitization
exports.processImage = async (filePath) => {
try {
// Re-encode image to strip metadata and validate
await sharp(filePath)
.jpeg({ quality: 85 })
.toFile(filePath + '.processed');
// Replace original with processed
fs.renameSync(filePath + '.processed', filePath);
} catch (error) {
// Invalid image, delete it
fs.unlinkSync(filePath);
throw new Error('Invalid image file');
}
};
module.exports = upload;Secrets Management
// config/secrets.js
const AWS = require('aws-sdk');
// Load secrets from AWS Secrets Manager
exports.loadSecrets = async () => {
const client = new AWS.SecretsManager({
region: process.env.AWS_REGION
});
try {
const data = await client.getSecretValue({
SecretId: process.env.SECRET_NAME
}).promise();
const secrets = JSON.parse(data.SecretString);
// Set environment variables
process.env.DATABASE_PASSWORD = secrets.DATABASE_PASSWORD;
process.env.JWT_SECRET = secrets.JWT_SECRET;
process.env.API_KEY = secrets.API_KEY;
} catch (error) {
console.error('Failed to load secrets:', error);
process.exit(1);
}
};
// Validate secrets exist and are strong
exports.validateSecrets = () => {
const required = [
'DATABASE_PASSWORD',
'JWT_SECRET',
'SESSION_SECRET'
];
required.forEach(secret => {
if (!process.env[secret]) {
throw new Error(`Missing required secret: ${secret}`);
}
if (process.env[secret].length < 32) {
throw new Error(`Weak secret: ${secret} (min 32 chars)`);
}
});
};Security Checklist
Pre-Production Checklist
- [ ] All secrets in environment variables (not code)
- [ ] Secrets are strong (32+ characters)
- [ ] HTTPS enforced (HSTS enabled)
- [ ] Security headers configured (Helmet)
- [ ] CORS properly configured
- [ ] Rate limiting implemented
- [ ] Input validation on all endpoints
- [ ] SQL injection prevention (parameterized queries)
- [ ] XSS prevention (sanitization + CSP)
- [ ] CSRF protection enabled
- [ ] Authentication uses bcrypt (12+ rounds)
- [ ] JWT tokens short-lived (15min access, 7day refresh)
- [ ] Session cookies secure + httpOnly + sameSite
- [ ] File uploads validated and sanitized
- [ ] Error messages don't leak info
- [ ] Logging excludes sensitive data
- [ ] Dependencies scanned (
npm audit) - [ ] Security headers tested (securityheaders.com)
- [ ] Penetration testing completed
Related Resources
Testing Strategies
Comprehensive testing guide for Express applications
Test Structure
Recommended Test Organization
tests/
├── setup.js # Global test setup
├── fixtures/
│ ├── users.json # Test data
│ └── posts.json
├── factories/
│ ├── userFactory.js # Test data generators
│ └── postFactory.js
├── helpers/
│ ├── authHelper.js # Auth test utilities
│ └── dbHelper.js # Database test utilities
├── unit/
│ ├── models/
│ ├── services/
│ └── utils/
├── integration/
│ ├── routes/
│ │ ├── users.test.js
│ │ ├── auth.test.js
│ │ └── posts.test.js
│ └── middleware/
└── e2e/
└── userFlows.test.jsUnit Testing
Testing Services
// tests/unit/services/userService.test.js
const userService = require('../../../src/services/userService');
const User = require('../../../src/models/User');
jest.mock('../../../src/models/User');
describe('UserService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('createUser', () => {
it('should create user with hashed password', async () => {
const userData = {
email: 'test@example.com',
password: 'Password123'
};
const mockUser = {
id: 1,
email: userData.email,
password: 'hashed_password'
};
User.create.mockResolvedValue(mockUser);
const result = await userService.createUser(userData);
expect(User.create).toHaveBeenCalledWith(
expect.objectContaining({
email: userData.email,
password: expect.not.stringMatching(userData.password)
})
);
expect(result).toEqual(mockUser);
});
it('should throw error for duplicate email', async () => {
const userData = {
email: 'duplicate@example.com',
password: 'Password123'
};
User.create.mockRejectedValue(
new Error('Duplicate email')
);
await expect(
userService.createUser(userData)
).rejects.toThrow('Duplicate email');
});
});
describe('validatePassword', () => {
it('should return true for valid password', async () => {
const user = {
password: '$2b$12$hashed_password'
};
const plainPassword = 'Password123';
const isValid = await userService.validatePassword(
plainPassword,
user.password
);
expect(isValid).toBe(true);
});
it('should return false for invalid password', async () => {
const user = {
password: '$2b$12$hashed_password'
};
const plainPassword = 'WrongPassword';
const isValid = await userService.validatePassword(
plainPassword,
user.password
);
expect(isValid).toBe(false);
});
});
});Testing Utilities
// tests/unit/utils/validation.test.js
const { validateEmail, validatePassword } = require('../../../src/utils/validation');
describe('Validation Utils', () => {
describe('validateEmail', () => {
it('should accept valid email', () => {
expect(validateEmail('user@example.com')).toBe(true);
});
it('should reject invalid email', () => {
expect(validateEmail('invalid-email')).toBe(false);
expect(validateEmail('user@')).toBe(false);
expect(validateEmail('@example.com')).toBe(false);
});
});
describe('validatePassword', () => {
it('should accept strong password', () => {
expect(validatePassword('Password123!')).toBe(true);
});
it('should reject weak password', () => {
expect(validatePassword('weak')).toBe(false);
expect(validatePassword('password')).toBe(false);
expect(validatePassword('12345678')).toBe(false);
});
});
});Integration Testing
Testing Routes with Supertest
// tests/integration/routes/users.test.js
const request = require('supertest');
const app = require('../../../src/app');
const { createUser } = require('../../factories/userFactory');
const { getAuthToken } = require('../../helpers/authHelper');
describe('User Routes', () => {
describe('GET /api/users', () => {
it('should return paginated users', async () => {
await createUser({ email: 'user1@example.com' });
await createUser({ email: 'user2@example.com' });
await createUser({ email: 'user3@example.com' });
const response = await request(app)
.get('/api/users')
.query({ page: 1, limit: 2 })
.expect(200);
expect(response.body).toHaveProperty('users');
expect(response.body.users).toHaveLength(2);
expect(response.body).toHaveProperty('total', 3);
expect(response.body).toHaveProperty('page', 1);
expect(response.body).toHaveProperty('pages', 2);
});
it('should filter users by query', async () => {
await createUser({ name: 'John Doe' });
await createUser({ name: 'Jane Smith' });
const response = await request(app)
.get('/api/users')
.query({ search: 'John' })
.expect(200);
expect(response.body.users).toHaveLength(1);
expect(response.body.users[0].name).toBe('John Doe');
});
});
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
const userData = {
email: 'newuser@example.com',
name: 'New User',
password: 'Password123!'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body.user).toMatchObject({
email: userData.email,
name: userData.name
});
expect(response.body.user).not.toHaveProperty('password');
expect(response.body).toHaveProperty('token');
});
it('should return 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
name: 'User',
password: 'Password123!'
})
.expect(400);
expect(response.body).toHaveProperty('errors');
expect(response.body.errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
field: 'email',
message: expect.stringContaining('valid email')
})
])
);
});
it('should return 409 for duplicate email', async () => {
await createUser({ email: 'existing@example.com' });
await request(app)
.post('/api/users')
.send({
email: 'existing@example.com',
name: 'User',
password: 'Password123!'
})
.expect(409);
});
});
describe('PUT /api/users/:id', () => {
it('should update user with valid token', async () => {
const user = await createUser();
const token = await getAuthToken(user);
const response = await request(app)
.put(`/api/users/${user.id}`)
.set('Authorization', `Bearer ${token}`)
.send({ name: 'Updated Name' })
.expect(200);
expect(response.body.user.name).toBe('Updated Name');
});
it('should return 401 without token', async () => {
const user = await createUser();
await request(app)
.put(`/api/users/${user.id}`)
.send({ name: 'Updated Name' })
.expect(401);
});
it('should return 403 when updating other user', async () => {
const user1 = await createUser({ email: 'user1@example.com' });
const user2 = await createUser({ email: 'user2@example.com' });
const token = await getAuthToken(user1);
await request(app)
.put(`/api/users/${user2.id}`)
.set('Authorization', `Bearer ${token}`)
.send({ name: 'Hacked' })
.expect(403);
});
});
});Testing Middleware
// tests/integration/middleware/auth.test.js
const request = require('supertest');
const app = require('../../../src/app');
const { createUser } = require('../../factories/userFactory');
const { signToken } = require('../../../src/utils/jwt');
describe('Authentication Middleware', () => {
describe('authenticate', () => {
it('should allow request with valid token', async () => {
const user = await createUser();
const token = signToken({ id: user.id });
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.user.id).toBe(user.id);
});
it('should reject request without token', async () => {
await request(app)
.get('/api/protected')
.expect(401);
});
it('should reject request with invalid token', async () => {
await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
it('should reject request with expired token', async () => {
const expiredToken = signToken(
{ id: 1 },
{ expiresIn: '-1h' }
);
await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
});
describe('authorize', () => {
it('should allow admin access', async () => {
const admin = await createUser({ role: 'admin' });
const token = signToken({ id: admin.id, role: 'admin' });
await request(app)
.delete('/api/users/123')
.set('Authorization', `Bearer ${token}`)
.expect(204);
});
it('should deny non-admin access', async () => {
const user = await createUser({ role: 'user' });
const token = signToken({ id: user.id, role: 'user' });
await request(app)
.delete('/api/users/123')
.set('Authorization', `Bearer ${token}`)
.expect(403);
});
});
});Test Helpers and Utilities
Authentication Helper
// tests/helpers/authHelper.js
const { signToken } = require('../../src/utils/jwt');
exports.getAuthToken = (user) => {
return signToken({
id: user.id,
email: user.email,
role: user.role || 'user'
});
};
exports.getExpiredToken = (user) => {
return signToken(
{ id: user.id },
{ expiresIn: '-1h' }
);
};
exports.authenticatedRequest = (request, user) => {
const token = exports.getAuthToken(user);
return request.set('Authorization', `Bearer ${token}`);
};Database Helper
// tests/helpers/dbHelper.js
const mongoose = require('mongoose');
exports.cleanDatabase = async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
};
exports.closeDatabase = async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
};
exports.clearCollection = async (collectionName) => {
const collection = mongoose.connection.collections[collectionName];
if (collection) {
await collection.deleteMany();
}
};Test Factories
// tests/factories/userFactory.js
const User = require('../../src/models/User');
const bcrypt = require('bcrypt');
let userCounter = 0;
exports.createUser = async (overrides = {}) => {
userCounter++;
const defaultData = {
email: `user${userCounter}@example.com`,
name: `User ${userCounter}`,
password: await bcrypt.hash('Password123!', 10),
role: 'user'
};
const userData = { ...defaultData, ...overrides };
return User.create(userData);
};
exports.createUsers = async (count, overrides = {}) => {
const users = [];
for (let i = 0; i < count; i++) {
users.push(await exports.createUser(overrides));
}
return users;
};
exports.createAdmin = async (overrides = {}) => {
return exports.createUser({ role: 'admin', ...overrides });
};Mocking Strategies
Mocking External APIs
// tests/integration/externalApi.test.js
const nock = require('nock');
const request = require('supertest');
const app = require('../../src/app');
describe('External API Integration', () => {
afterEach(() => {
nock.cleanAll();
});
it('should fetch data from external API', async () => {
// Mock external API
nock('https://api.example.com')
.get('/data')
.reply(200, {
data: 'external data'
});
const response = await request(app)
.get('/api/external-data')
.expect(200);
expect(response.body.data).toBe('external data');
});
it('should handle external API failure', async () => {
nock('https://api.example.com')
.get('/data')
.reply(500);
await request(app)
.get('/api/external-data')
.expect(503);
});
});Mocking Database
// Use mongodb-memory-server for integration tests
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
// Or mock Mongoose models for unit tests
jest.mock('../../src/models/User');
const User = require('../../src/models/User');
User.findById.mockResolvedValue({
id: '123',
email: 'test@example.com'
});Test Coverage
Jest Configuration
// jest.config.js
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.js',
'!src/tests/**',
'!src/migrations/**'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testMatch: [
'**/tests/**/*.test.js'
],
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
clearMocks: true,
resetMocks: true,
restoreMocks: true
};Running Coverage
# Run tests with coverage
npm run test:coverage
# View coverage report
open coverage/lcov-report/index.html
# CI/CD coverage threshold enforcement
npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'E2E Testing
User Flow Testing
// tests/e2e/userFlows.test.js
const request = require('supertest');
const app = require('../../src/app');
describe('User Registration and Login Flow', () => {
it('should complete full user lifecycle', async () => {
const userData = {
email: 'flow@example.com',
name: 'Flow User',
password: 'Password123!'
};
// 1. Register
const registerResponse = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(201);
expect(registerResponse.body).toHaveProperty('token');
const userId = registerResponse.body.user.id;
// 2. Login
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: userData.email,
password: userData.password
})
.expect(200);
const token = loginResponse.body.token;
// 3. Get profile
const profileResponse = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(profileResponse.body.user.email).toBe(userData.email);
// 4. Update profile
await request(app)
.put(`/api/users/${userId}`)
.set('Authorization', `Bearer ${token}`)
.send({ name: 'Updated Name' })
.expect(200);
// 5. Logout
await request(app)
.post('/api/auth/logout')
.set('Authorization', `Bearer ${token}`)
.expect(204);
// 6. Verify token invalidated
await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${token}`)
.expect(401);
});
});Performance Testing
Load Testing with Artillery
# artillery.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 10
name: 'Warm up'
- duration: 120
arrivalRate: 50
name: 'Sustained load'
- duration: 60
arrivalRate: 100
name: 'Peak load'
scenarios:
- name: 'API Load Test'
flow:
- get:
url: '/api/users'
- post:
url: '/api/users'
json:
email: '{{ $randomString() }}@example.com'
name: 'Test User'
password: 'Password123!'# Run load test
artillery run artillery.yml
# Generate HTML report
artillery run --output report.json artillery.yml
artillery report report.jsonBest Practices
1. Test Structure: Unit → Integration → E2E 2. Test Independence: Each test should be isolated 3. Clear Naming: Describe what, when, expected outcome 4. Setup/Teardown: Use beforeEach/afterEach for cleanup 5. Factory Pattern: Use factories for test data 6. Mock External Deps: Don't hit real APIs in tests 7. Coverage Targets: Aim for 80%+ coverage 8. Fast Tests: Keep test suite under 30 seconds 9. Parallel Execution: Run tests in parallel when possible 10. CI Integration: Run tests on every commit
Related Resources
Related skills
FAQ
What does express-production improve in Express apps?
express-production targets validation, middleware composition, security headers, structured errors, and operations patterns in Express.js. The skill helps developers close common production gaps without rewriting the entire Node service.
When should you run express-production on a codebase?
Run express-production when an Express API functions in development but lacks production guardrails before staging or production cutover. Skip it if the team is migrating off Express to another Node framework.