DEV Community

Sudhansu Bandha
Sudhansu Bandha

Posted on

Building a Scalable Modular Node.js Express Backend Structure (With GitHub Repo)

As a developer, it's often frustrating to work with a codebase that isn't modular. It becomes hard to understand how the code flows and even harder to maintain or make changes without breaking things.

I wanted to learn how to build a modular codebase to avoid these problems. This also gives me a chance to try out new ideas and improve based on what I’ve experienced in past projects.

Why Modular Architecture?

A modular backend architecture allows:

  • Separation of concerns (routes, services, config, etc.)
  • Easier maintenance and testing
  • Faster onboarding for new developers
  • Flexibility for microservice conversion

Project Structure Overview

Here's the high-level folder structure:
modular_codebase/
├── src(apis)/
│ ├── modules(domains)/
│ │ └── v1/
│ │ ├── index.js
│ │ ├── user/
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── routes/
│ │ │ └── index.js
│ │ └── orders/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── routes/
│ │ └── index.js
│ ├── shared/
│ │ ├── middlewares/
│ │ └── utils/
│ ├── config/
│ │ └── database.js
├── .env
└── server.js

Explanation

  1. We are organizing our application into feature-based modules, where each module contains its own set of routes, controllers, services, helpers, and middlewares.
  2. These modules are grouped under API versioning directories (e.g., /v1) to allow for scalable and backward-compatible API development.
  3. The shared folder holds common functionality that is reused across multiple modules, such as utility functions and global middlewares.
  4. The config directory is used for storing configuration files like database connection strings and other environment-specific settings.

Automatic Route Registration in Action

To avoid the repetitive task of manually registering routes for every module, I’ve implemented a simple automation that dynamically wires up all routes based on the folder structure. Whenever a new module is created (e.g., user, orders, etc.) and it follows the standard directory structure, its routes are auto-mounted to the corresponding path (like /api/v1/user).

In my application, routes aren’t directly defined using router.get(...). Instead, each route is represented as a function returning a config object like this:

    getUsersDetails() {
        return {
            method: "GET",
            path: "/:id/details",
            middlewares: [authMiddleware],
            handler: (req, res) => {
                UsersController.getUserDetails(req, res);
            },
        };
    }
Enter fullscreen mode Exit fullscreen mode

A shared RouteBuilder utility class was created to dynamically generate route definitions for each module. This utility is extended by individual modules to define their specific routes in a clean and consistent way.

class GenerateRoutesForController{
    getRoutes(){
        const routesHandlerNames =   
         Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(name =>
            typeof this[name] === 'function' && name !== 'constructor'
        );

        return routesHandlerNames.map(handlerName => this[handlerName]())

    }
}

Enter fullscreen mode Exit fullscreen mode

Each module contains a parent index.js file that is responsible for dynamically generating all available routes for that module. This is achieved using an Immediately Invoked Function Expression (IIFE), ensuring that routes are registered as soon as the module is loaded. This automation removes the need for manual imports or route registration

const express = require("express");
const router = express.Router();

const mr = require("./routes");

//Generate Routes using IIFE

(() => {
    Object.keys(mr).forEach((el) => {
        const routes = mr[el].getRoutes();
        routes.forEach((route) => {
            router[route.method.toLowerCase()](
                route.path,
                ...(route.middlewares || []),
                route.handler
            );
        });
    });
})();

module.exports = {
    router,
    basePath: "/users",
};

Enter fullscreen mode Exit fullscreen mode

All module routes are registered in app.js using the structure returned from each module’s index.js. The basePath defined in each route object is used to construct the full route path for the module.

const registerRoutes = (version, routes) => {
    Object.keys(routes).forEach((key) => {
        app.use(`/${version}${routes[key].basePath}`, routes[key].router);
    });
};

// Register the v1 routes
registerRoutes("v1", v1Routes);
Enter fullscreen mode Exit fullscreen mode

This is my take on building a modular codebase for a Node.js Express application. I'm open to feedback and suggestions to improve it further.

In the future, I plan to:

  1. Integrate database support for both MongoDB and PostgreSQL
  2. Evolve this modular monolith into a microservices-oriented architecture using Docker for containerization
  3. Orchestrate and scale these microservices using Kubernetes for production-ready deployments

You can find the project on GitHub here: [https://github.com/SudhansuBandha/modular_codebase].
Feel free to explore or use it as a learning resource!

Top comments (0)