Deploying Express applications to production requires careful attention to performance, security, and reliability. This guide covers essential best practices.
Environment Configuration
Set NODE_ENV to production
export NODE_ENV = production
This enables:
View template caching
Less verbose error messages
Performance optimizations in many packages
Use environment variables
require ( 'dotenv' ). config ();
const config = {
port: process . env . PORT || 3000 ,
dbUrl: process . env . DATABASE_URL ,
sessionSecret: process . env . SESSION_SECRET
};
Validate configuration on startup
const requiredEnvVars = [
'DATABASE_URL' ,
'SESSION_SECRET' ,
'API_KEY'
];
requiredEnvVars . forEach ( varName => {
if ( ! process . env [ varName ]) {
console . error ( `Missing required environment variable: ${ varName } ` );
process . exit ( 1 );
}
});
Enable Compression
Compress response bodies to reduce bandwidth:
const compression = require ( 'compression' );
const express = require ( 'express' );
const app = express ();
app . use ( compression ());
Use a Reverse Proxy
Use nginx or Apache as a reverse proxy to handle static files, SSL termination, load balancing, and caching.
nginx Configuration
Trust Proxy in Express
upstream app {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80 ;
server_name example.com;
# Redirect HTTP to HTTPS
return 301 https://$ server_name $ request_uri ;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Serve static files directly
location /static/ {
alias /path/to/static/;
expires 1y;
add_header Cache-Control "public, immutable" ;
}
# Proxy to Express app
location / {
proxy_pass http://app;
proxy_http_version 1.1 ;
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection 'upgrade' ;
proxy_set_header Host $ host ;
proxy_cache_bypass $ http_upgrade ;
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 ;
}
}
Enable Caching
const express = require ( 'express' );
const app = express ();
// Cache static files
app . use ( express . static ( 'public' , {
maxAge: '1y' ,
etag: true ,
lastModified: true
}));
// Cache views in production
if ( app . get ( 'env' ) === 'production' ) {
app . enable ( 'view cache' );
}
Process Management
Use a Process Manager
Use PM2 to manage your Node.js processes, handle crashes, and enable zero-downtime deployments.
npm install -g pm2
# Start app with PM2
pm2 start app.js -i max
# Start with ecosystem file
pm2 start ecosystem.config.js
module . exports = {
apps: [{
name: 'express-app' ,
script: './app.js' ,
instances: 'max' ,
exec_mode: 'cluster' ,
env: {
NODE_ENV: 'production' ,
PORT: 3000
},
error_file: './logs/err.log' ,
out_file: './logs/out.log' ,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
};
Graceful Shutdown
const express = require ( 'express' );
const app = express ();
const server = app . listen ( 3000 );
// Handle shutdown signals
process . on ( 'SIGTERM' , gracefulShutdown );
process . on ( 'SIGINT' , gracefulShutdown );
function gracefulShutdown () {
console . log ( 'Received shutdown signal, closing server...' );
server . close (() => {
console . log ( 'HTTP server closed' );
// Close database connections
mongoose . connection . close ( false , () => {
console . log ( 'MongoDB connection closed' );
process . exit ( 0 );
});
});
// Force shutdown after 10 seconds
setTimeout (() => {
console . error ( 'Forcing shutdown' );
process . exit ( 1 );
}, 10000 );
}
Logging
Don’t use console.log in production. Use a proper logging library.
npm install winston morgan
Winston Logger
HTTP Request Logging
const winston = require ( 'winston' );
const logger = winston . createLogger ({
level: process . env . LOG_LEVEL || 'info' ,
format: winston . format . combine (
winston . format . timestamp (),
winston . format . errors ({ stack: true }),
winston . format . json ()
),
transports: [
new winston . transports . File ({ filename: 'error.log' , level: 'error' }),
new winston . transports . File ({ filename: 'combined.log' })
]
});
if ( process . env . NODE_ENV !== 'production' ) {
logger . add ( new winston . transports . Console ({
format: winston . format . simple ()
}));
}
module . exports = logger ;
Error Handling
const logger = require ( './logger' );
// 404 handler
app . use (( req , res , next ) => {
res . status ( 404 ). json ({ error: 'Not found' });
});
// Error handler
app . use (( err , req , res , next ) => {
// Log error
logger . error ({
message: err . message ,
stack: err . stack ,
url: req . url ,
method: req . method
});
// Don't leak error details in production
const statusCode = err . status || 500 ;
const message = process . env . NODE_ENV === 'production'
? 'Internal server error'
: err . message ;
res . status ( statusCode ). json ({ error: message });
});
Database Connection Pooling
MongoDB with Mongoose
PostgreSQL with node-postgres
const mongoose = require ( 'mongoose' );
mongoose . connect ( process . env . DATABASE_URL , {
maxPoolSize: 10 ,
minPoolSize: 5 ,
socketTimeoutMS: 45000 ,
serverSelectionTimeoutMS: 5000
});
Health Checks
app . get ( '/health' , async ( req , res ) => {
const healthcheck = {
uptime: process . uptime (),
message: 'OK' ,
timestamp: Date . now ()
};
try {
// Check database connection
await mongoose . connection . db . admin (). ping ();
healthcheck . database = 'connected' ;
res . status ( 200 ). json ( healthcheck );
} catch ( error ) {
healthcheck . database = 'disconnected' ;
healthcheck . message = error . message ;
res . status ( 503 ). json ( healthcheck );
}
});
const helmet = require ( 'helmet' );
const express = require ( 'express' );
const app = express ();
app . use ( helmet ());
app . disable ( 'x-powered-by' );
// Enable CORS for specific origins
const cors = require ( 'cors' );
app . use ( cors ({
origin: process . env . ALLOWED_ORIGINS ?. split ( ',' ),
credentials: true
}));
Request Size Limits
const express = require ( 'express' );
const app = express ();
// Limit request body size
app . use ( express . json ({ limit: '10mb' }));
app . use ( express . urlencoded ({ extended: true , limit: '10mb' }));
Monitoring
Application Performance Monitoring
const client = require ( 'prom-client' );
// Create metrics
const httpRequestDuration = new client . Histogram ({
name: 'http_request_duration_seconds' ,
help: 'Duration of HTTP requests in seconds' ,
labelNames: [ 'method' , 'route' , 'status_code' ]
});
// Track metrics
app . use (( req , res , next ) => {
const start = Date . now ();
res . on ( 'finish' , () => {
const duration = ( Date . now () - start ) / 1000 ;
httpRequestDuration . labels ( req . method , req . route ?. path || req . path , res . statusCode ). observe ( duration );
});
next ();
});
// Expose metrics endpoint
app . get ( '/metrics' , async ( req , res ) => {
res . set ( 'Content-Type' , client . register . contentType );
res . end ( await client . register . metrics ());
});
Production Checklist