Node.js has revolutionized server-side JavaScript development, but writing Node.js code is easy while writing good Node.js code is an art. As applications grow in complexity, poorly structured code can quickly become a maintenance nightmare. In this article, we'll explore 10 essential Node.js best practices that will help you build robust, scalable, and maintainable applications.
1. Embrace Async/Await Over Callbacks
The callback hell is a common pitfall for Node.js developers. While callbacks are fundamental to Node's non-blocking I/O model, they can lead to unreadable and hard-to-maintain code.
// Bad - Callback pyramid
fs.readFile('file1.txt', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', (err, data2) => {
if (err) throw err;
fs.readFile('file3.txt', (err, data3) => {
if (err) throw err;
console.log(data1, data2, data3);
});
});
});
// Good - Async/Await
async function readFiles() {
try {
const data1 = await fs.promises.readFile('file1.txt');
const data2 = await fs.promises.readFile('file2.txt');
const data3 = await fs.promises.readFile('file3.txt');
console.log(data1, data2, data3);
} catch (err) {
console.error(err);
}
}
2. Implement Proper Error Handling
Uncaught exceptions can crash your Node.js application. Always handle errors gracefully.
// Process-level error handling
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
// Perform cleanup and exit
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Handle or exit
});
3. Use Environment Variables for Configuration
Hardcoding configuration values is a security risk and makes your application less portable.
// Using dotenv for environment variables
require('dotenv').config();
const PORT = process.env.PORT || 3000;
const API_KEY = process.env.VEDIKA_API_KEY;
// Example with Vedika API
const vedikaQuery = async (question, birthDetails) => {
try {
const response = await fetch('https://api.vedika.io/v1/astrology/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
question,
birthDetails
})
});
return await response.json();
} catch (error) {
console.error('Vedika API error:', error);
throw error;
}
};
4. Structure Your Application with Modular Architecture
Organize your code into logical modules to improve maintainability and reusability.
src/
├── config/
│ ├── database.js
│ └── vedika.js
├── controllers/
│ └── astrologyController.js
├── models/
│ └── birthChart.js
├── routes/
│ └── api.js
├── services/
│ └── vedikaService.js
├── utils/
│ └── errorHandler.js
└── app.js
5. Implement Logging for Debugging and Monitoring
Proper logging is crucial for debugging and monitoring your application in production.
const winston = require('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' })
]
});
// Example usage with Vedika API call
const logVedikaQuery = async (question, birthDetails) => {
logger.info('Initiating Vedika astrology query', { question, birthDetails });
try {
const result = await vedikaQuery(question, birthDetails);
logger.info('Vedika query successful', { questionId: result.id });
return result;
} catch (error) {
logger.error('Vedika query failed', { error, question, birthDetails });
throw error;
}
};
6. Validate User Input
Never trust user input. Always validate and sanitize it before processing.
const Joi = require('joi');
const birthDetailsSchema = Joi.object({
datetime: Joi.date().iso().required(),
latitude: Joi.number().min(-90).max(90).required(),
longitude: Joi.number().min(-180).max(180).required()
});
const questionSchema = Joi.string().min(5).max(200).required();
const validateAstrologyRequest = (req, res, next) => {
const { error } = Joi.object({
question: questionSchema,
birthDetails: birthDetailsSchema
}).validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details
});
}
next();
};
7. Use Connection Pooling for Database Operations
Creating new database connections for each request is inefficient. Use connection pooling instead.
const { Pool } = require('pg');
// Create a connection pool
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
max: 20, // Maximum number of connections
idleTimeoutMillis: 30000, // Close idle connections after 30 seconds
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
});
// Example query using the pool
async function getUserBirthChart(userId) {
const client = await pool.connect();
try {
const result = await client.query(
'SELECT * FROM birth_charts WHERE user_id = $1',
[userId]
);
return result.rows[0];
} finally {
client.release();
}
}
8. Implement Rate Limiting
Protect your API from abuse by implementing rate limiting.
const rateLimit = require('express-rate-limit');
const apiLimiter = 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'
});
// Apply rate limiting to Vedika API routes
app.use('/api/vedika', apiLimiter);
9. Optimize Performance with Caching
Reduce response time and load on your servers by implementing caching.
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600, checkperiod: 600 }); // Cache for 10 minutes
const getCachedVedikaResponse = async (cacheKey, question, birthDetails) => {
const cachedResponse = cache.get(cacheKey);
if (cachedResponse) {
console.log('Returning cached response');
return cachedResponse;
}
console.log('Fetching fresh response from Vedika API');
const response = await vedikaQuery(question, birthDetails);
// Cache the response
cache.set(cacheKey, response);
return response;
};
10. Use PM2 for Process Management
PM2 is a production process manager for Node.js applications that helps you keep applications alive forever, reloads them without downtime, and facilitates common system admin tasks.
# Install PM2 globally
npm install pm2 -g
# Start your application with PM2
pm2 start app.js -i max
# Monitor your application
pm2 monit
# View logs
pm2 logs
# Save process list for startup
pm2 save
pm2 startup
Conclusion
Implementing these best practices will significantly improve the quality of your Node.js applications. Start by incorporating one or two practices at a time, gradually building a robust development workflow.
For next steps:
- Set up a CI/CD pipeline to automate testing and deployment
- Implement comprehensive unit and integration tests
- Monitor your application in production with tools like Prometheus and Grafana
- Consider using TypeScript for better type safety and developer experience
Remember that good code is not just about functionality—it's about maintainability, scalability, and performance. By following these practices, you'll be well on your way to becoming a proficient Node.js developer.
Top comments (0)