DEV Community

Vedika Intelligence
Vedika Intelligence

Posted on • Originally published at vedika.io

Beyond the Basics: 10 Node.js Best Practices for Production-Ready Applications

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Set up a CI/CD pipeline to automate testing and deployment
  2. Implement comprehensive unit and integration tests
  3. Monitor your application in production with tools like Prometheus and Grafana
  4. 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)