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.
Proper error handling is crucial for building reliable applications. Always handle errors gracefully and provide meaningful error messages.
Always handle errors in async functions
1// ✅ Good - Proper error handling2async 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 appropriately9 }10}1112// ❌ Bad - No error handling13async function getData() {14 const result = await fetchData(); // Unhandled promise rejection!15 return result;16}
Use centralized error handling in Express
1// Error handling middleware (put this last)2app.use((err, req, res, next) => {3 console.error(err.stack);45 res.status(err.status || 500).json({6 error: {7 message: err.message,8 // Only show stack in development9 stack: process.env.NODE_ENV === 'development' ? err.stack : undefined10 }11 });12});
Handle unhandled rejections
1// Catch unhandled promise rejections2process.on('unhandledRejection', (reason, promise) => {3 console.error('Unhandled Rejection at:', promise, 'reason:', reason);4 // Application specific logging, throwing an error, or other logic here5});67// Catch uncaught exceptions8process.on('uncaughtException', (error) => {9 console.error('Uncaught Exception:', error);10 process.exit(1); // Exit to restart11});
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 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:
npm install dotenv
Create a .env file:
DATABASE_URL=postgresql://user:pass@localhost/dbAPI_KEY=your-secret-api-keyJWT_SECRET=your-jwt-secretPORT=3000
Use in your application:
1require('dotenv').config();23const dbUrl = process.env.DATABASE_URL;4const apiKey = process.env.API_KEY;5const port = process.env.PORT || 3000;
Never commit .env files!
Add to .gitignore:
.env.env.local.env.*.local
Validate and sanitize user input
1const validator = require('validator');23app.post('/user', (req, res) => {4 const { email, name, age } = req.body;56 // Validate email7 if (!validator.isEmail(email)) {8 return res.status(400).json({ error: 'Invalid email' });9 }1011 // Sanitize input12 const sanitizedName = validator.escape(name);1314 // Validate age15 if (!Number.isInteger(age) || age < 0 || age > 150) {16 return res.status(400).json({ error: 'Invalid age' });17 }1819 // 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)
Write efficient code that scales well and provides a great user experience.
Use async operations for I/O
1// ✅ Good - Non-blocking2const fs = require('fs').promises;34async function readFiles() {5 // Read files in parallel6 const [file1, file2] = await Promise.all([7 fs.readFile('file1.txt', 'utf8'),8 fs.readFile('file2.txt', 'utf8')9 ]);10 return { file1, file2 };11}1213// ❌ Bad - Blocking14const fs = require('fs');15const file1 = fs.readFileSync('file1.txt', 'utf8'); // Blocks!16const file2 = fs.readFileSync('file2.txt', 'utf8'); // Blocks!
Implement caching
1// Simple in-memory cache2const cache = new Map();3const CACHE_TTL = 60000; // 1 minute45async function getUser(id) {6 const cached = cache.get(id);78 if (cached && Date.now() - cached.timestamp < CACHE_TTL) {9 return cached.data;10 }1112 const user = await database.findUser(id);13 cache.set(id, { data: user, timestamp: Date.now() });1415 return user;16}
Use compression
1const compression = require('compression');2const express = require('express');3const app = express();45// Enable gzip compression6app.use(compression());78// 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
Organize your project for maintainability and scalability. A well-structured codebase is easier to understand, test, and extend.
Recommended project structure
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
1// ✅ Good - Separated concerns2// controllers/userController.js3const userService = require('../services/userService');45exports.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};1314// services/userService.js15const User = require('../models/User');1617exports.findById = async (id) => {18 return await User.findById(id);19};2021// routes/users.js22const express = require('express');23const router = express.Router();24const userController = require('../controllers/userController');2526router.get('/:id', userController.getUser);2728module.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!