Scalable Directory Structure for NodeJS + Express Web Servers
This blog explores a scalable folder structure for Node.js Express servers using Docker, TypeScript, and domain-driven design. Learn how to organize key components like configurations, domains, middlewares, and utilities to ensure maintainability, modularity, and scalability in your application.

When building an enterprise-grade Node.js application, a well-organized folder structure is crucial for maintainability and scalability. The folder structure should not only reflect the modularity of the app but also accommodate growth. In this blog, we’ll break down a scalable directory structure for Node.js Express servers, specifically one that supports Docker, and TypeScript, and includes domain-driven design patterns.
Let's take a look at the structure that we’ll be discussing:
.
├── .devcontainer/
├── node_modules/
├── src/
│ ├── configs/
│ ├── jobs/
│ ├── domains/
│ ├── libs/
│ ├── middlewares/
│ ├── routes/
│ ├── templates/
│ ├── utils/
│ ├── index.ts
│ └── server.ts
├── .dockerignore
├── .env
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.dev
├── nodemon.json
├── package.json
├── README.md
├── tsconfig.json
└── yarn.lock
Before moving in, if you are just starting a node project, you might want to check out this article as well.

Root Level Configuration Files
The root directory contains essential configuration files that allow the application to run efficiently in different environments.
- Dockerfile & docker-compose.yml: These files allow for containerization of the application for consistent development environments and deployment.
Dockerfile.dev
is used for local development, where additional toolsnodemon
are often installed for live reloading. - .dockerignore: Specifies files and directories that Docker should ignore to keep the image lean.
- .env: Stores environment variables such as database URLs and API keys, often used by the
dotenv
package to manage configuration. If you have env variables different for different environments, you can separate that as well. - .gitignore: Ensures that sensitive files (like
node_modules
, logs, etc.) are not committed to version control. - tsconfig.json: Configures TypeScript compilation, defining how TypeScript should compile
.ts
files into JavaScript. - package.json & yarn.lock: Package management files, with
package.json
describing project dependencies, scripts, and metadata, andyarn.lock
ensuring consistent dependency versions.
Inside src
Directory
The src/
folder is where the core logic resides. Let's explore its sub-folders in detail.
a) configs/
This folder holds all configuration-related files, such as database configuration, API settings, or any external service configurations.
Example:
db.config.ts
– Database connection settings.app.config.ts
– General application configuration.
b) jobs/
For applications that rely on scheduled tasks, this folder holds your cron jobs. These are scheduled tasks that might run daily, weekly, or periodically.
Example:
emailScheduler.ts
– A cron job that sends reminder emails daily.
c) domains/
This is a key folder in this structure because it supports a domain-driven design approach. In the domains folder, each folder represents a specific domain model in your application (e.g., users
, orders
, etc.).
Each domain folder contains:
- Model: Defines the schema or business rules of the domain (e.g., Mongoose schema ).
- Routes: Handles incoming HTTP requests for the domain, often Express routes.
- Controller: Contains the logic that executes when a specific route is hit.
- Types: TypeScript interfaces or types that define the shape of data in this domain.
For example, the users/
folder might look like:
src/
└── domains/
└── users/
├── user.model.ts
├── user.routes.ts
├── user.controller.ts
└── user.types.ts
And please note the user.routes.ts file below:
import express from "express";
import { changeEmail, changeLanguage, changeTheme } from "./controllers";
const userRouter = express.Router();
userRouter.put("/change-email", changeEmail);
userRouter.put("/change-theme", changeTheme);
userRouter.put("/change-language", changeLanguage);
export { userRouter };
This approach enables a clear separation of concerns. Each domain is self-contained, promoting modularity and making it easy to scale.
d) libs/
The libs/
folder contains reusable libraries or modules that can be used across different parts of the application. For example, custom helpers or utility classes or library functions could be housed here.
Example:
cors.ts
– A utility for defining the origin of the requests.bodyParser.ts
– A utility for parsing the request body.
e) middlewares/
Middlewares in Express are functions that execute during the lifecycle of a request before the route handler. This folder is for all custom middleware logic, such as authentication, error handling, or validation.
Example:
authMiddleware.ts
– Middleware that checks if the user is authenticated before accessing certain routes.
f) routes/
While individual domain routes can live in their respective folders (under domains/
), this folder consolidates the top-level route structure for the application. For example, this folder could define a central router that merges all domain-specific routes into one.
Example:
index.ts
– The file that imports and consolidates all routes from thedomains
and exports them as a single Express Router.
import express from "express";
import { userRouter } from "../domains/user/routes";
import { authRouter } from "../domains/auth/routes";
const mainRouter = express.Router();
mainRouter.use("/auth", authRouter);
mainRouter.use("/user", authorise, userRouter);
export default mainRouter;
Did you notice? This is much more modular in terms of adding more features. This mainRouter
can then be used inside server.ts
g) templates/
This folder stores HTML templates or other static assets that can be used by your application, especially if it generates server-side content or emails.
Example:
emailTemplate.html
– HTML template for system-generated emails.
h) utils/
This folder contains utility functions that don’t fit into other categories, such as formatting functions, mathematical helpers, or other common code that’s used across the app.
Example:
dateUtils.ts
– A utility to format dates or perform date-based operations.
i) index.ts & server.ts
- index.ts: The main entry point that starts your application. Typically, this file imports your server configuration and starts listening for requests.
- server.ts: This is where your Express server is configured, and middlewares, routes, and other app-wide configurations are applied.
import express from "express";
import { corsFix } from "./libs/cors";
import { requestBodyParser } from "./libs/bodyParser";
import cookieParser from "./libs/cookieParser";
import mainRouter from "./routes";
import httpLogger from "./libs/morgan";
const app = express();
app.use(httpLogger);
app.use(corsFix);
app.use(requestBodyParser);
app.use(cookieParser);
app.use(mainRouter);
app.get("/", (req, res) => {
res.send("The Server is Running Fine 🚀");
});
export default app;
server.ts
Key benefits of this structure
- Modularity and Scalability: The separation into domains and clear folder structure allows the application to scale horizontally. Adding a new feature or domain doesn’t require refactoring the whole application but simply adding a new folder in the
domains/
folder. - Single Responsibility: Each folder (and sub-folder) has a clear responsibility. This ensures that developers working on a specific part of the application don't need to understand the entire system to contribute.
- Ease of Collaboration: Teams working on separate parts of the app (e.g., user management vs. product management) can focus on specific domains without touching unrelated parts of the codebase.
- Docker Support: Docker configurations at the root level ensure that the application can be containerized easily, making it simple to run the app in any environment without worrying about dependencies.
- TypeScript Support: TypeScript configurations and clear type definitions ensure type safety, which helps in catching errors early and maintaining a more predictable codebase.
A well-structured directory is the backbone of a scalable and maintainable Node.js application. By following a domain-driven design, using separate folders for configurations, middlewares, and utilities, and leveraging Docker and TypeScript, you ensure that your application can scale smoothly while remaining easy to manage.
This structure not only helps during development but also makes onboarding new developers easier. It ensures the app is well-organized and ready for future growth.
That's it for today. See ya 👋