title: "Node.js Best Practices for Production Applications"
date: 2026-04-10
readingTime: 3 min read
tags: ["Back End", "Node.js", "Tools"]
After years of building and maintaining Node.js applications in production, here are the essential practices every developer should follow.
async function getUser(id: string) {
try {
const user = await db.users.findById(id);
if (!user) {
throw new NotFoundError('User not found');
}
return user;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
// Log and re-throw
logger.error('Failed to fetch user', { error, userId: id });
throw new InternalServerError('Failed to fetch user');
}
}
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error('Unhandled error', { error: err });
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
});
}
res.status(500).json({
error: 'Internal server error',
});
});
import helmet from 'helmet';
app.use(helmet());
import rateLimit from 'express-rate-limit';
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, please try again later.',
});
app.use('/api/', limiter);
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1).max(50),
});
app.post('/users', (req, res) => {
const result = userSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.errors });
}
// Process valid data
createUser(result.data);
});
import Redis from 'ioredis';
const redis = new Redis();
async function getProduct(id: string) {
const cached = await redis.get(`product:${id}`);
if (cached) {
return JSON.parse(cached);
}
const product = await db.products.findById(id);
await redis.setex(`product:${id}`, 3600, JSON.stringify(product));
return product;
}
// ❌ Bad: N+1 query problem
const users = await db.users.findAll();
for (const user of users) {
user.posts = await db.posts.findByUserId(user.id);
}
// ✅ Good: Eager loading
const users = await db.users.findAll({
include: ['posts'],
});
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: 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(),
}));
}
app.get('/health', async (req, res) => {
const healthCheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
database: await checkDatabaseConnection(),
memoryUsage: process.memoryUsage(),
};
res.status(200).json(healthCheck);
});
import { config } from 'dotenv';
config();
export const env = {
NODE_ENV: process.env.NODE_ENV || 'development',
PORT: parseInt(process.env.PORT || '3000', 10),
DATABASE_URL: process.env.DATABASE_URL || '',
REDIS_URL: process.env.REDIS_URL || '',
JWT_SECRET: process.env.JWT_SECRET || '',
};
if (!env.DATABASE_URL) {
throw new Error('DATABASE_URL is required');
}
Following these best practices will help you build robust, secure, and performant Node.js applications. Remember: good practices are not optional in production!
What are your favorite Node.js best practices? Share them in the comments below!