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
- We are organizing our application into feature-based modules, where each module contains its own set of routes, controllers, services, helpers, and middlewares.
- These modules are grouped under API versioning directories (e.g., /v1) to allow for scalable and backward-compatible API development.
- The shared folder holds common functionality that is reused across multiple modules, such as utility functions and global middlewares.
- 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);
},
};
}
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]())
}
}
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",
};
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);
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:
- Integrate database support for both MongoDB and PostgreSQL
- Evolve this modular monolith into a microservices-oriented architecture using Docker for containerization
- 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)