Best Coding Practices for Node.js and Express.js

Admin | Xyvin - Oct 30 - - Dev Community

Node.js, combined with Express.js, has become a go-to framework for building fast, scalable web applications. To ensure reliability, performance, and maintainability, it’s essential to follow best coding practices when developing with Node and Express. In this guide, we’ll cover key practices to write clean, efficient, and robust code that can scale with ease.

1. Organize Your Project Structure

A well-structured project is the foundation of a maintainable codebase. Node doesn’t enforce a particular structure, so it’s up to developers to keep things organized.

Basic Directory Layout:

├── controllers
├── models
├── routes
├── middlewares
├── utils
└── app.js
Enter fullscreen mode Exit fullscreen mode
  • Controllers: Handle requests and responses.
  • Models: Define data structures (typically for MongoDB, MySQL, etc.).
  • Routes: Define all application routes.
  • Middlewares: Hold reusable functions (e.g., authentication, error handling).
  • Utils: For helper functions or constants. This organization ensures that responsibilities are clear, which makes debugging and scaling easier.

2. Use Environment Variables

Hardcoding sensitive information, such as database credentials, in code is insecure and inflexible. Use .env files to manage configuration data.

DB_HOST=localhost
DB_USER=root
DB_PASS=secret
JWT_SECRET=my_jwt_secret
Enter fullscreen mode Exit fullscreen mode

Usage in Code

require('dotenv').config();
const dbHost = process.env.DB_HOST;
Enter fullscreen mode Exit fullscreen mode

This practice also allows for easy configuration changes between development and production environments without altering the source code.

3. Handle Errors Gracefully

Express has robust error-handling capabilities. Use middleware to capture errors and provide meaningful responses.

Basic Error Handling Middleware:

// Middleware for catching errors
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
});
Enter fullscreen mode Exit fullscreen mode

Async Error Handling

When using async functions, wrap them in a try-catch block to avoid unhandled promise rejections:

app.get('/route', async (req, res, next) => {
  try {
    const data = await someAsyncFunction();
    res.json(data);
  } catch (error) {
    next(error); // Passes errors to the error-handling middleware
  }
});
Enter fullscreen mode Exit fullscreen mode

Centralizing error handling makes troubleshooting easier and ensures consistent responses to the client.

4. Follow RESTful API Design

Following REST principles makes your API predictable and easy to understand. Here are some essential rules:

Use proper HTTP verbs:

- GET for retrieving resources.
- POST for creating resources.
- PUT/PATCH for updating resources.
- DELETE for deleting resources.
Enter fullscreen mode Exit fullscreen mode
  • Use nouns in URLs instead of actions (e.g., /users instead of /getUsers).
  • Nest URLs logically (e.g., /users/:userId/orders/:orderId).

Keeping your API RESTful improves readability and consistency, especially when working with frontend teams.

5. Utilize Middleware

Express middleware enhances code modularity by letting you apply reusable functions to requests.

Examples of Common Middleware Uses:

  • Authentication and Authorization: For example, JWT-based authentication can be applied to protected routes.
const authenticate = (req, res, next) => {
  // Logic for token verification
  next();
};

app.use('/protected', authenticate);
Enter fullscreen mode Exit fullscreen mode
  • Data Validation: Use libraries like Joi to validate incoming data.
const Joi = require('joi');
const schema = Joi.object({ name: Joi.string().min(3).required() });

app.post('/data', (req, res, next) => {
  const { error } = schema.validate(req.body);
  if (error) return res.status(400).json({ error: error.details[0].message });
  next();
});
Enter fullscreen mode Exit fullscreen mode

Using middleware keeps your main application logic clean and focused.

6. Use Async/Await and Avoid Callback Hell

Callbacks can make your code difficult to read and prone to bugs. async/await simplifies asynchronous code and makes it more readable.

// Instead of nested callbacks
doSomething((err, result1) => {
  if (err) return handleError(err);
  doSomethingElse(result1, (err, result2) => {
    if (err) return handleError(err);
    doFinalThing(result2, (err, finalResult) => {
      if (err) return handleError(err);
      console.log(finalResult);
    });
  });
});

// Use async/await for better readability
const process = async () => {
  try {
    const result1 = await doSomething();
    const result2 = await doSomethingElse(result1);
    const finalResult = await doFinalThing(result2);
    console.log(finalResult);
  } catch (error) {
    handleError(error);
  }
};
Enter fullscreen mode Exit fullscreen mode

This practice reduces errors and makes your asynchronous code flow more intuitive.

7. Leverage a Linter and Formatter

Using a linter like ESLint ensures that your code adheres to a consistent style and helps catch common errors. A formatter like Prettier can keep your code visually consistent.

Set up ESLint and Prettier:


npm install eslint prettier eslint-config-prettier eslint-plugin-prettier --save-dev
Enter fullscreen mode Exit fullscreen mode

Add configuration files for ESLint and Prettier in your project root to define your coding style and enforce standards.

Automated code formatting and linting reduce the likelihood of errors and improve readability for your team.

8. Limit Payload Sizes and Rate Limit API Requests

Restrict the size of JSON payloads to prevent abuse and ensure application security. You can use express.json() to limit payload sizes:

app.use(express.json({ limit: '10kb' })); // Limit requests to 10kb
Enter fullscreen mode Exit fullscreen mode

Implement Rate Limiting: Use a package like express-rate-limit to prevent DDoS attacks by limiting the number of requests a client can make.


const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use(limiter);
Enter fullscreen mode Exit fullscreen mode

Rate limiting protects your API from excessive usage, helping maintain performance and security.

9. Optimize Database Queries and Caching

Database queries can be time-consuming, especially when handling large datasets. Use indexing, optimized queries, and caching to minimize load times.

Caching Example: Use a service like Redis to cache data that doesn’t change frequently.

const redis = require('redis');
const client = redis.createClient();

const cacheMiddleware = (req, res, next) => {
  const { id } = req.params;
  client.get(id, (err, data) => {
    if (err) throw err;
    if (data) {
      res.send(JSON.parse(data));
    } else {
      next();
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Caching helps improve response times and reduces load on the database.

10. Write Tests for Your Code

Testing is essential for a robust application. Use a testing framework like Jest or Mocha to create unit and integration tests for your application.

Basic Jest Test Example:


const request = require('supertest');
const app = require('../app');

describe('GET /users', () => {
  it('should return an array of users', async () => {
    const response = await request(app).get('/users');
    expect(response.statusCode).toBe(200);
    expect(Array.isArray(response.body)).toBeTruthy();
  });
});
Enter fullscreen mode Exit fullscreen mode

Writing tests ensures that new changes don’t break existing functionality, enabling you to develop with confidence.

Conclusion

Following best practices when working with Node.js and Express.js is essential for building secure, scalable, and maintainable applications. By organizing your project, handling errors properly, using environment variables, following REST principles, and leveraging middleware, you create a codebase that’s efficient and easy to manage. Taking steps like caching data, rate-limiting API requests, and writing tests ensures that your application performs reliably under various conditions.

As your project grows, revisiting and refining these practices will help maintain the quality of your application and make it easy to work on as part of a team.

. . .