Node.js Best Practices

Follow these industry standards to build robust, secure, and maintainable Node.js applications. Learn error handling, security, performance optimization, and code organization.

Introduction
Security First
Always validate input, use environment variables for secrets, and keep dependencies updated.
Optimize Performance
Use async operations, implement caching, and monitor your application's performance.
Clean Code
Follow consistent coding standards, write meaningful comments, and keep functions small.
Configuration
Use environment-specific configs and never hardcode sensitive information.
Error Handling

Proper error handling is crucial for building reliable applications. Always handle errors gracefully and provide meaningful error messages.

Always handle errors in async functions

js
1// ✅ Good - Proper error handling
2async function getData() {
3 try {
4 const result = await fetchData();
5 return result;
6 } catch (error) {
7 console.error('Error fetching data:', error);
8 throw error; // or handle appropriately
9 }
10}
11
12// ❌ Bad - No error handling
13async function getData() {
14 const result = await fetchData(); // Unhandled promise rejection!
15 return result;
16}

Use centralized error handling in Express

js
1// Error handling middleware (put this last)
2app.use((err, req, res, next) => {
3 console.error(err.stack);
4
5 res.status(err.status || 500).json({
6 error: {
7 message: err.message,
8 // Only show stack in development
9 stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
10 }
11 });
12});

Handle unhandled rejections

js
1// Catch unhandled promise rejections
2process.on('unhandledRejection', (reason, promise) => {
3 console.error('Unhandled Rejection at:', promise, 'reason:', reason);
4 // Application specific logging, throwing an error, or other logic here
5});
6
7// Catch uncaught exceptions
8process.on('uncaughtException', (error) => {
9 console.error('Uncaught Exception:', error);
10 process.exit(1); // Exit to restart
11});

Important

Never expose detailed error messages or stack traces to clients in production! Log them server-side but send generic error messages to users.

Security

Security should be a top priority. Follow these practices to protect your application and user data.

Use environment variables for sensitive data

Install the dotenv package:

bash
npm install dotenv

Create a .env file:

text
DATABASE_URL=postgresql://user:pass@localhost/db
API_KEY=your-secret-api-key
JWT_SECRET=your-jwt-secret
PORT=3000

Use in your application:

js
1require('dotenv').config();
2
3const dbUrl = process.env.DATABASE_URL;
4const apiKey = process.env.API_KEY;
5const port = process.env.PORT || 3000;

Never commit .env files!

Add to .gitignore:

text
.env
.env.local
.env.*.local

Validate and sanitize user input

js
1const validator = require('validator');
2
3app.post('/user', (req, res) => {
4 const { email, name, age } = req.body;
5
6 // Validate email
7 if (!validator.isEmail(email)) {
8 return res.status(400).json({ error: 'Invalid email' });
9 }
10
11 // Sanitize input
12 const sanitizedName = validator.escape(name);
13
14 // Validate age
15 if (!Number.isInteger(age) || age < 0 || age > 150) {
16 return res.status(400).json({ error: 'Invalid age' });
17 }
18
19 // Process sanitized data...
20});

Security best practices checklist

  • Use HTTPS in production
  • Implement rate limiting to prevent DDoS attacks
  • Use helmet.js for secure HTTP headers
  • Keep dependencies updated (npm audit)
  • Use parameterized queries to prevent SQL injection
  • Implement proper authentication and authorization
  • Hash passwords with bcrypt
  • Use CORS properly
  • Set secure cookie flags (httpOnly, secure, sameSite)
Performance Optimization

Write efficient code that scales well and provides a great user experience.

Use async operations for I/O

js
1// ✅ Good - Non-blocking
2const fs = require('fs').promises;
3
4async function readFiles() {
5 // Read files in parallel
6 const [file1, file2] = await Promise.all([
7 fs.readFile('file1.txt', 'utf8'),
8 fs.readFile('file2.txt', 'utf8')
9 ]);
10 return { file1, file2 };
11}
12
13// ❌ Bad - Blocking
14const fs = require('fs');
15const file1 = fs.readFileSync('file1.txt', 'utf8'); // Blocks!
16const file2 = fs.readFileSync('file2.txt', 'utf8'); // Blocks!

Implement caching

js
1// Simple in-memory cache
2const cache = new Map();
3const CACHE_TTL = 60000; // 1 minute
4
5async function getUser(id) {
6 const cached = cache.get(id);
7
8 if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
9 return cached.data;
10 }
11
12 const user = await database.findUser(id);
13 cache.set(id, { data: user, timestamp: Date.now() });
14
15 return user;
16}

Use compression

js
1const compression = require('compression');
2const express = require('express');
3const app = express();
4
5// Enable gzip compression
6app.use(compression());
7
8// Your routes...

Performance tips

  • Use connection pooling for databases
  • Implement pagination for large datasets
  • Use streaming for large files
  • Enable HTTP/2 for better performance
  • Use a CDN for static assets
  • Implement proper logging without blocking
  • Use cluster module to utilize multiple CPU cores
  • Monitor performance with tools like PM2 or New Relic
Code Structure & Organization

Organize your project for maintainability and scalability. A well-structured codebase is easier to understand, test, and extend.

Recommended project structure

text
my-app/
├── src/
│ ├── controllers/ # Request handlers
│ ├── models/ # Data models
│ ├── routes/ # Route definitions
│ ├── middleware/ # Custom middleware
│ ├── services/ # Business logic
│ ├── utils/ # Helper functions
│ ├── config/ # Configuration files
│ └── app.js # Express app setup
├── tests/ # Test files
├── public/ # Static files
├── .env # Environment variables
├── .env.example # Example env file
├── .gitignore
├── package.json
└── README.md

Separate concerns

js
1// ✅ Good - Separated concerns
2// controllers/userController.js
3const userService = require('../services/userService');
4
5exports.getUser = async (req, res, next) => {
6 try {
7 const user = await userService.findById(req.params.id);
8 res.json(user);
9 } catch (error) {
10 next(error);
11 }
12};
13
14// services/userService.js
15const User = require('../models/User');
16
17exports.findById = async (id) => {
18 return await User.findById(id);
19};
20
21// routes/users.js
22const express = require('express');
23const router = express.Router();
24const userController = require('../controllers/userController');
25
26router.get('/:id', userController.getUser);
27
28module.exports = router;

Code quality best practices

  • Follow the Single Responsibility Principle
  • Keep functions small and focused
  • Use meaningful variable and function names
  • Write comments for complex logic only
  • Use ESLint for code consistency
  • Write unit tests for critical functions
  • Use Prettier for code formatting
  • Follow consistent naming conventions
  • Avoid deep nesting (max 3 levels)
  • Don't repeat yourself (DRY principle)

Ready to Build?

Now that you know the best practices, let's put everything together and build a complete REST API with Express.js!

Built for learning — keep experimenting!