Skip to main content

Node.js Pro Tips in Practice

Environment Variable Management (dotenv)

Installing and Basic Usage of dotenv

npm install dotenv
# .env file (never commit this to git)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=my-very-secret-key-change-in-production
REDIS_URL=redis://localhost:6379
API_KEY=sk-1234567890abcdef
// Load at the very top of the app entry point
require('dotenv').config();

// Or specify a path
require('dotenv').config({ path: '.env.local' });

// Use environment variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;

if (!jwtSecret) {
throw new Error('JWT_SECRET environment variable is not set');
}

Managing Environment-Specific Config Files

.env                  # Defaults (can commit to git, exclude sensitive info)
.env.local # Local overrides (excluded from git)
.env.development # Development environment
.env.production # Production environment
.env.test # Test environment
.env.example # Example file (commit to git, use placeholder values)
# .env.example (template for team members)
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://localhost:5432/mydb
JWT_SECRET=change-me-in-production
// config.js — centralized environment variable management
require('dotenv').config();

const config = {
env: process.env.NODE_ENV || 'development',
isDev: process.env.NODE_ENV !== 'production',
port: Number(process.env.PORT) || 3000,

db: {
url: required('DATABASE_URL'),
poolSize: Number(process.env.DB_POOL_SIZE) || 10,
},

jwt: {
secret: required('JWT_SECRET'),
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},

redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
},
};

function required(key) {
const value = process.env[key];
if (!value) {
throw new Error(`Required environment variable missing: ${key}`);
}
return value;
}

module.exports = config;

PM2 Process Management

PM2 is a production process manager for Node.js. It supports automatic restarts, log management, and cluster mode.

npm install -g pm2

Basic Commands

# Start an app
pm2 start app.js
pm2 start app.js --name "my-api"

# Multiple instances (cluster mode)
pm2 start app.js -i max # as many as CPU cores
pm2 start app.js -i 4 # 4 instances

# Check status
pm2 status
pm2 list

# View logs
pm2 logs # all logs
pm2 logs my-api # logs for a specific app
pm2 logs --lines 100 # last 100 lines

# Restart / stop / delete
pm2 restart my-api
pm2 reload my-api # zero-downtime restart
pm2 stop my-api
pm2 delete my-api

# Monitoring dashboard
pm2 monit

# Auto-restart on memory limit
pm2 start app.js --max-memory-restart 500M

PM2 Configuration File

// ecosystem.config.js
module.exports = {
apps: [
{
name: 'api-server',
script: 'src/index.js',
instances: 'max', // as many as CPU cores
exec_mode: 'cluster', // cluster mode
watch: false, // false in production
max_memory_restart: '500M', // restart if memory exceeds 500MB

env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 8080,
},

log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true, // merge cluster logs

// Auto-restart settings
autorestart: true,
restart_delay: 4000, // wait 4 seconds before restarting
max_restarts: 10,
min_uptime: '5s', // considered started normally if running for 5+ seconds
},
{
name: 'worker',
script: 'src/worker.js',
instances: 2,
exec_mode: 'fork',
cron_restart: '0 3 * * *', // restart every day at 3 AM
},
],
};
# Start using config file
pm2 start ecosystem.config.js
pm2 start ecosystem.config.js --env production

# Auto-start on system reboot
pm2 startup
pm2 save # save current process list

Cluster Mode: Utilizing CPU Cores

Node.js is single-threaded, but you can leverage multiple CPU cores using the cluster module.

// cluster.js
const cluster = require('cluster');
const os = require('os');
const http = require('http');

const numCPUs = os.cpus().length;

if (cluster.isPrimary) {
console.log(`Master process PID: ${process.pid}`);
console.log(`CPU cores: ${numCPUs} → creating ${numCPUs} workers`);

// Create workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

// Restart worker on exit
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} exited (code: ${code}, signal: ${signal})`);
console.log('Creating new worker...');
cluster.fork();
});

// Worker ready event
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} started`);
});
} else {
// Worker process
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
pid: process.pid,
message: `Handled by worker ${process.pid}`,
}));
});

server.listen(3000, () => {
console.log(`Worker ${process.pid}: listening on port 3000`);
});
}

Zero-Downtime Deployment Pattern

// cluster-reload.js
const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
const numCPUs = os.cpus().length;

// Start initial workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

// Zero-downtime restart via SIGUSR2 signal
process.on('SIGUSR2', () => {
console.log('Starting zero-downtime restart...');
const workers = Object.values(cluster.workers);

// Restart workers one by one
function reloadWorker(index) {
if (index >= workers.length) {
console.log('Zero-downtime restart complete');
return;
}

const worker = workers[index];
console.log(`Restarting worker ${worker.process.pid}...`);

// Start new worker, then shut down old one
const newWorker = cluster.fork();
newWorker.once('listening', () => {
worker.disconnect();
worker.once('exit', () => {
reloadWorker(index + 1);
});
});
}

reloadWorker(0);
});
}

// Restart command
// kill -SIGUSR2 $(cat app.pid)

Error Handling: uncaughtException, unhandledRejection

// error-handling.js

// Unhandled Promise rejection
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Promise rejection:');
console.error(' Reason:', reason);
// Log and exit the process
// Node.js 15+ exits the process by default
process.exit(1);
});

// Uncaught exception
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err.message);
console.error(err.stack);

// Important: after uncaughtException, process state is unstable
// Always exit and let PM2/k8s restart the process
process.exit(1);
});

// Graceful shutdown handling
let isShuttingDown = false;

function gracefulShutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;

console.log(`${signal} received, shutting down...`);

// Close server
server.close(() => {
console.log('HTTP server closed');

// Close DB connection
// database.close().then(() => {
// console.log('DB connection closed');
// process.exit(0);
// });

process.exit(0);
});

// Force exit timeout
setTimeout(() => {
console.error('Forced exit (30 second timeout)');
process.exit(1);
}, 30_000).unref(); // unref: prevents this timer from blocking the event loop
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C

Performance Monitoring

Node.js Built-in Inspector

# Enable inspector
node --inspect app.js
node --inspect=0.0.0.0:9229 app.js # remote access
node --inspect-brk app.js # break on first line

# Connect via Chrome DevTools
# chrome://inspect
// Performance measurement
const { performance, PerformanceObserver } = require('perf_hooks');

// Mark-based measurement
performance.mark('start');
// ... work ...
performance.mark('end');
performance.measure('Task duration', 'start', 'end');

const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
}
});
obs.observe({ entryTypes: ['measure'] });

// v8 heap snapshot
const v8 = require('v8');
const stats = v8.getHeapStatistics();
console.log({
heapSizeLimit: `${(stats.heap_size_limit / 1024 / 1024).toFixed(0)} MB`,
totalHeapSize: `${(stats.total_heap_size / 1024 / 1024).toFixed(0)} MB`,
usedHeapSize: `${(stats.used_heap_size / 1024 / 1024).toFixed(0)} MB`,
});

Profiling with clinic.js

npm install -g clinic

# CPU profiling
clinic doctor -- node app.js

# Event loop blocking detection
clinic bubbleprof -- node app.js

# Flame graph
clinic flame -- node app.js

Security Basics

Helmet.js: HTTP Security Headers

npm install helmet
const express = require('express');
const helmet = require('helmet');

const app = express();

// Apply all default security headers
app.use(helmet());

// Individual configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'cdn.example.com'],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31_536_000, // 1 year
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' }, // X-Frame-Options: DENY
noSniff: true, // X-Content-Type-Options: nosniff
xssFilter: true, // X-XSS-Protection
}));

Rate Limiting

npm install express-rate-limit
const rateLimit = require('express-rate-limit');

// Global limit
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 200,
message: { error: 'Too many requests' },
standardHeaders: true,
legacyHeaders: false,
skip: (req) => req.ip === '127.0.0.1', // exclude local IP
});

// Stricter limit for login endpoint
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // max 10 attempts
message: { error: 'Too many login attempts, retry after 1 hour' },
skipSuccessfulRequests: true, // don't count successful requests
});

app.use(globalLimiter);
app.post('/auth/login', loginLimiter, loginHandler);

Input Validation and SQL Injection Prevention

// SQL injection prevention: use parameterized queries
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Unsafe approach (never do this)
// const result = await pool.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// Safe approach: parameterized query
async function getUser(id) {
const result = await pool.query(
'SELECT id, name, email FROM users WHERE id = $1',
[id] // automatically escaped
);
return result.rows[0];
}

// XSS prevention: escape on output
function escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

worker_threads: CPU-Intensive Tasks

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const os = require('os');

// Fibonacci calculation (CPU-intensive)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

if (isMainThread) {
// Main thread
async function runWithWorker(n) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, {
workerData: { n }
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker exit code: ${code}`));
});
});
}

// Process multiple tasks in parallel
Promise.all([
runWithWorker(40),
runWithWorker(41),
runWithWorker(42),
]).then(results => {
console.log('Results:', results);
});
} else {
// Worker thread
const { n } = workerData;
const result = fibonacci(n);
parentPort.postMessage(result);
}

Additional Tips for Experts

Debugging Tips

// Debug built-in modules with NODE_DEBUG environment variable
// NODE_DEBUG=fs,http node app.js

// Custom debug namespace (debug package)
// npm install debug
const debug = require('debug');
const log = debug('app:server');
const dbLog = debug('app:db');

log('Server started'); // DEBUG=app:* node app.js

// util.debuglog (built-in)
const util = require('util');
const appLog = util.debuglog('app');
appLog('This message only prints when NODE_DEBUG=app is set');

Detecting Memory Leaks

// Common causes of memory leaks
// 1. Accumulation of global variables
// 2. Event listeners not removed
// 3. Large data captured in closures
// 4. Timers not cleared

// Preventing event listener leaks
const emitter = new EventEmitter();

function onData(data) { /* ... */ }
emitter.on('data', onData);

// Always remove after use
emitter.off('data', onData);

// Or use once
emitter.once('data', onData);

// Clearing timers
const timer = setInterval(() => {}, 1000);
// On app shutdown
clearInterval(timer);