Express Production Configuration
Introduction
When transitioning your Express.js application from development to production, proper configuration is crucial for ensuring performance, security, and reliability. Development environments are optimized for developer convenience, while production environments need to focus on application stability, resource efficiency, and security.
This guide covers essential configuration changes and best practices that you should implement before deploying your Express application to a production environment. We'll explore environment-specific settings, performance optimizations, security enhancements, and error handling strategies.
Environment Variables
Environment variables allow you to configure your application differently based on the environment it's running in.
Setting Up Environment Variables
A popular approach is using the dotenv
package to manage environment variables:
npm install dotenv
Create different .env
files for different environments:
# .env.development
NODE_ENV=development
PORT=3000
DEBUG=app:*
# .env.production
NODE_ENV=production
PORT=8080
In your application, load the appropriate environment file:
// app.js
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config({ path: `.env.${process.env.NODE_ENV || 'development'}` });
} else {
require('dotenv').config({ path: '.env.production' });
}
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
Performance Optimizations
Enabling Compression
Compression can significantly reduce the size of the response body, making your application faster:
npm install compression
const express = require('express');
const compression = require('compression');
const app = express();
// Enable compression
app.use(compression());
HTTP Caching
Implement proper HTTP caching to reduce server load and improve response times:
app.use(express.static('public', {
maxAge: '1d', // Cache static assets for 1 day
etag: true, // Enable ETags
}));
Using a Process Manager
Process managers like PM2 help keep your application running and can optimize performance:
npm install -g pm2
Create a ecosystem.config.js
file for PM2:
module.exports = {
apps: [{
name: 'my-express-app',
script: 'app.js',
instances: 'max', // Use all available CPUs
exec_mode: 'cluster', // Run in cluster mode
env_production: {
NODE_ENV: 'production',
PORT: 8080
}
}]
};
Start your application using PM2:
pm2 start ecosystem.config.js --env production
Security Enhancements
Setting Security Headers
Use the Helmet middleware to set secure HTTP headers:
npm install helmet
const helmet = require('helmet');
// Set security headers
app.use(helmet());
Implementing Rate Limiting
Protect your application from brute-force attacks by implementing rate limiting:
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// Basic rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
// Apply rate limiting to all requests
app.use(limiter);
// Apply stricter limits to specific routes
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts from this IP, please try again after 15 minutes'
});
app.post('/login', loginLimiter, loginController.handleLogin);
Trust Proxy
If you're running Express behind a proxy like Nginx, make sure Express trusts the proxy:
app.set('trust proxy', 1); // trust first proxy
Error Handling
Production Error Handler
Create a separate error handler for production to avoid leaking sensitive information:
// Development error handler (with details)
if (process.env.NODE_ENV === 'development') {
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({
message: err.message,
error: err
});
});
}
// Production error handler (no stacktraces leaked to user)
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({
message: err.message,
error: {}
});
});
Handling Uncaught Exceptions
Ensure your application doesn't crash due to uncaught exceptions:
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
// Perform cleanup if needed
process.exit(1); // Exit with failure
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging
});
Logging in Production
Structured Logging
Use a logging library like Winston for structured logging:
npm install winston
const winston = require('winston');
// Configure logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
// Write to console in development
...(process.env.NODE_ENV !== 'production'
? [new winston.transports.Console()]
: []),
// Write to file in production
...(process.env.NODE_ENV === 'production'
? [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
: [])
]
});
// Example usage
app.get('/', (req, res) => {
logger.info('Home page accessed', {
ip: req.ip,
user: req.user?.id
});
res.send('Hello World');
});
Complete Production Setup Example
Here's a comprehensive example that ties together all the concepts:
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const winston = require('winston');
const path = require('path');
// Load environment variables based on NODE_ENV
require('dotenv').config({
path: `.env.${process.env.NODE_ENV || 'development'}`
});
// Initialize Express app
const app = express();
// Configure logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
...(process.env.NODE_ENV !== 'production'
? [new winston.transports.Console()]
: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
)
]
});
// Trust proxy if behind reverse proxy
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}
// Apply security headers
app.use(helmet());
// Enable gzip compression
app.use(compression());
// Basic rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Serve static files with caching
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1d',
etag: true
}));
// Routes
app.get('/', (req, res) => {
logger.info('Home page accessed');
res.send('Hello World');
});
// API routes (example)
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));
// 404 handler
app.use((req, res, next) => {
res.status(404).json({
message: 'Resource not found'
});
});
// Error handler
app.use((err, req, res, next) => {
// Log error
logger.error('Application error', {
error: process.env.NODE_ENV === 'development' ? err : err.message,
url: req.originalUrl
});
// Send error response
res.status(err.status || 500);
res.json({
message: err.message,
error: process.env.NODE_ENV === 'development' ? err : {}
});
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
// Handle uncaught exceptions and rejections
process.on('uncaughtException', (err) => {
logger.error('Uncaught Exception:', err);
process.exit(1); // Exit with failure
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', {
promise: promise,
reason: reason
});
});
Production Deployment Checklist
Before deploying, ensure you've:
- ✅ Set
NODE_ENV=production
- ✅ Implemented proper error handling
- ✅ Added security headers with Helmet
- ✅ Enabled compression
- ✅ Configured rate limiting
- ✅ Set up structured logging
- ✅ Managed environment variables securely
- ✅ Configured static file caching
- ✅ Chosen a process manager (PM2, forever, etc.)
- ✅ Implemented HTTPS (in production)
Summary
Configuring Express for production involves several key aspects:
- Environment variables to manage different settings across environments
- Performance optimizations like compression and clustering
- Security enhancements including Helmet, rate limiting, and input validation
- Robust error handling to prevent application crashes
- Structured logging for debugging and monitoring
By implementing these production configurations, your Express application will be more secure, performant, and reliable in a production environment.
Additional Resources
Practice Exercises
- Create a complete Express application with separate configuration files for development and production environments.
- Implement a logging system that writes to the console in development but to files in production.
- Set up PM2 to manage an Express application in cluster mode.
- Implement route-specific rate limiting for sensitive endpoints like authentication.
- Create a middleware that monitors response times and logs slow responses for performance analysis.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)