Essential guidelines for building secure, performant, and maintainable Node.js applications
This guide covers essential best practices for Node.js development, from security and error handling to performance optimization and production deployment. These patterns are based on real-world experience and industry standards.
Whether you're building REST APIs, microservices, or full-stack applications, following these practices will help you write better, more maintainable Node.js code.
A well-organized project structure makes code maintainable and scalable.
my-app/ ├── src/ │ ├── config/ # Configuration files │ │ └── database.js │ ├── controllers/ # Route controllers │ │ └── userController.js │ ├── middleware/ # Custom middleware │ │ ├── auth.js │ │ └── errorHandler.js │ ├── models/ # Data models │ │ └── User.js │ ├── routes/ # API routes │ │ └── userRoutes.js │ ├── services/ # Business logic │ │ └── userService.js │ ├── utils/ # Utility functions │ │ └── logger.js │ └── app.js # Express app setup ├── tests/ # Test files │ ├── unit/ │ └── integration/ ├── .env.example # Environment variables template ├── .gitignore ├── package.json └── server.js # Entry point
try { await doSomething(); } catch (err) { // Silent failure - BAD! }
try { await doSomething(); } catch (err) { logger.error('Failed:', err); throw err; // or handle appropriately }
// middleware/errorHandler.js class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } const errorHandler = (err, req, res, next) => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; if (process.env.NODE_ENV === 'development') { res.status(err.statusCode).json({ status: err.status, error: err, message: err.message, stack: err.stack, }); } else { // Production: Don't leak error details res.status(err.statusCode).json({ status: err.status, message: err.isOperational ? err.message : 'Something went wrong', }); } }; // Handle unhandled promise rejections process.on('unhandledRejection', (err) => { console.error('UNHANDLED REJECTION! 💥', err); process.exit(1); }); module.exports = { AppError, errorHandler };
// utils/catchAsync.js - Eliminates try/catch in every route const catchAsync = (fn) => { return (req, res, next) => { fn(req, res, next).catch(next); }; }; // Usage in controller const getUser = catchAsync(async (req, res, next) => { const user = await User.findById(req.params.id); if (!user) { return next(new AppError('User not found', 404)); } res.status(200).json({ data: user }); });
const express = require('express'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const mongoSanitize = require('express-mongo-sanitize'); const xss = require('xss-clean'); const app = express(); // Set security HTTP headers app.use(helmet()); // Rate limiting 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); // Body parser with size limit app.use(express.json({ limit: '10kb' })); // Data sanitization against NoSQL injection app.use(mongoSanitize()); // Data sanitization against XSS app.use(xss()); // Prevent parameter pollution app.use(hpp());
// config/env.js require('dotenv').config(); const requiredEnvVars = [ 'NODE_ENV', 'PORT', 'DATABASE_URL', 'JWT_SECRET', ]; requiredEnvVars.forEach((varName) => { if (!process.env[varName]) { throw new Error(`Missing required environment variable: ${varName}`); } }); module.exports = { nodeEnv: process.env.NODE_ENV, port: process.env.PORT || 3000, databaseUrl: process.env.DATABASE_URL, jwtSecret: process.env.JWT_SECRET, jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d', };
getData((err, data) => { if (err) return handleError(err); processData(data, (err, result) => { if (err) return handleError(err); saveResult(result, (err) => { // Nested callbacks... }); }); });
try { const data = await getData(); const result = await processData(data); await saveResult(result); } catch (err) { handleError(err); }
// ❌ Sequential - Slow (takes 3 seconds total) const user = await User.findById(id); const posts = await Post.find({ userId: id }); const comments = await Comment.find({ userId: id }); // ✅ Parallel - Fast (takes 1 second total) const [user, posts, comments] = await Promise.all([ User.findById(id), Post.find({ userId: id }), Comment.find({ userId: id }), ]); // Handle errors in parallel operations const results = await Promise.allSettled([ operation1(), operation2(), operation3(), ]); results.forEach((result, index) => { if (result.status === 'rejected') { console.error(`Operation ${index} failed:`, result.reason); } });
const compression = require('compression'); // Compress all responses app.use(compression()); // Custom compression settings app.use(compression({ level: 6, // Compression level (0-9) threshold: 1024, // Only compress responses > 1KB filter: (req, res) => { if (req.headers['x-no-compression']) { return false; } return compression.filter(req, res); }, }));
const mongoose = require('mongoose'); // Configure connection pool mongoose.connect(process.env.DATABASE_URL, { maxPoolSize: 10, // Maximum connections minPoolSize: 2, // Minimum connections serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, }); // Monitor connection events mongoose.connection.on('connected', () => { console.log('MongoDB connected'); }); mongoose.connection.on('error', (err) => { console.error('MongoDB connection error:', err); });
const redis = require('redis'); const client = redis.createClient(); // Cache middleware const cache = (duration) => { return async (req, res, next) => { const key = `cache:${req.originalUrl}`; try { const cachedData = await client.get(key); if (cachedData) { return res.json(JSON.parse(cachedData)); } // Store original res.json const originalJson = res.json.bind(res); res.json = (data) => { // Cache the response client.setEx(key, duration, JSON.stringify(data)); return originalJson(data); }; next(); } catch (err) { next(err); } }; }; // Usage: Cache for 5 minutes app.get('/api/posts', cache(300), getPostsController);
• Use clustering to utilize all CPU cores
• Enable gzip compression for responses
• Implement database indexing for faster queries
• Use CDN for static assets
• Monitor with tools like PM2, New Relic, or Datadog
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() ), defaultMeta: { service: 'my-app' }, transports: [ // Write errors to error.log new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), // Write all logs to combined.log new winston.transports.File({ filename: 'logs/combined.log' }), ], }); // Console logging in development if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), })); } // Usage logger.info('Server started', { port: 3000 }); logger.error('Database error', { error: err.message }); module.exports = logger;
// userService.test.js const userService = require('../services/userService'); const User = require('../models/User'); // Mock the User model jest.mock('../models/User'); describe('UserService', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('createUser', () => { it('should create a new user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', }; const mockUser = { id: 1, ...userData }; User.create.mockResolvedValue(mockUser); const result = await userService.createUser(userData); expect(User.create).toHaveBeenCalledWith(userData); expect(result).toEqual(mockUser); }); it('should throw error if email exists', async () => { User.create.mockRejectedValue(new Error('Email exists')); await expect( userService.createUser({ email: 'test@test.com' }) ).rejects.toThrow('Email exists'); }); }); });
| Category | Development | Production |
|---|---|---|
| Error Details | Show full stack traces | Hide internal errors |
| Logging | Console logs | File/service logs (Winston) |
| Dependencies | All (dev + prod) | Production only |
| Caching | Minimal/disabled | Aggressive caching |
| Compression | Optional | Always enabled |
| HTTPS | Optional | Required |
| Process Manager | Node directly | PM2/Docker |
// ecosystem.config.js module.exports = { apps: [{ name: 'my-app', script: './server.js', instances: 'max', // Use all CPU cores exec_mode: 'cluster', // Cluster mode env: { NODE_ENV: 'development' }, env_production: { NODE_ENV: 'production', PORT: 8080 }, error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss', merge_logs: true, autorestart: true, max_memory_restart: '1G', watch: false }] }; // Start: pm2 start ecosystem.config.js --env production
✓ Set NODE_ENV=production
✓ Enable HTTPS with valid SSL certificate
✓ Use process manager (PM2/Docker)
✓ Enable logging to files/services
✓ Set up monitoring and alerts
✓ Configure automatic restarts
✓ Use environment variables for secrets
✓ Enable rate limiting and CORS
✓ Set up database backups
✓ Configure health check endpoints