I am Sajan Acharya, a Senior Software Engineer based in Kathmandu. Teams ask me how to structure a Node.js backend when the codebase starts feeling like a drawers-full-of-everything problem: routes call models, models call services, services import controllers, and nobody knows where a new feature should live. Clean architecture is not ceremony for ceremony’s sake. It is a dependency rule: business logic stays independent of Express, MongoDB, Redis, and whatever queue you pick next quarter. This how-to shows the module-wise layout I use—domain, application, presentation, and infrastructure—so features grow as folders, not as spaghetti.
If you are still shaping API contracts and resource boundaries, pair this article with how to design scalable Node.js APIs. Structure without clear domain language still produces messy modules; clear domains without structure still produce tangled imports. You want both. For delivery help applying this layout on a real product, see my Node.js developer services.
The four layers, module by module
Organize by business module first—auth, user, billing, backtesting—then apply the same four layers inside each module. Domain holds entities, enums, and repository interfaces: pure rules with no framework imports. Application holds use cases, DTOs, ports, and factories that orchestrate those rules. Presentation adapts HTTP: controllers, route wiring, and validators. Infrastructure implements the ports—database models, repository adapters, security helpers, jobs, and external market-data clients. Dependencies point inward: presentation and infrastructure depend on application and domain; domain never imports Express or Mongoose.
- Domain: entities, enums, repository contracts—no HTTP, no ORM
- Application: use cases, DTOs, ports, factories—orchestration only
- Presentation: controllers, routes, validators—adapt the outside world
- Infrastructure: databases, security, jobs, third-party adapters
- Shared: cross-cutting config, middleware, responses, and utils used by many modules
A concrete Node.js folder tree
Below is a production-shaped layout. Top-level src/app holds process concerns—global middleware, route aggregation, and workers. src/modules owns features. src/shared owns cross-cutting pieces. src/types holds ambient or shared type declarations. Tooling folders (.cursor, .husky, .vscode) and build output stay at the repo root where they belong.
├── .cursor/
├── .husky/
├── .vscode/
├── dist/
├── node_modules/
├── src/
│ ├── app/
│ │ ├── middleware/
│ │ ├── routes/
│ │ └── workers/
│ │
│ ├── modules/
│ │ ├── auth/
│ │ │ ├── application/
│ │ │ │ ├── dto/
│ │ │ │ ├── factories/
│ │ │ │ ├── ports/
│ │ │ │ └── use-cases/
│ │ │ ├── domain/
│ │ │ │ ├── entities/
│ │ │ │ ├── enums/
│ │ │ │ └── repositories/
│ │ │ ├── infrastructure/
│ │ │ │ ├── database/
│ │ │ │ │ ├── models/
│ │ │ │ │ └── repositories/
│ │ │ │ ├── factories/
│ │ │ │ └── security/
│ │ │ └── presentation/
│ │ │ └── http/
│ │ │ ├── controllers/
│ │ │ ├── routes/
│ │ │ └── validators/
│ │ │
│ │ ├── backtesting/
│ │ │ ├── application/
│ │ │ │ ├── dto/
│ │ │ │ ├── factories/
│ │ │ │ ├── ports/
│ │ │ │ └── use-cases/
│ │ │ ├── domain/
│ │ │ │ ├── entities/
│ │ │ │ ├── enums/
│ │ │ │ ├── ports/
│ │ │ │ ├── repositories/
│ │ │ │ └── services/
│ │ │ │ └── strategies/
│ │ │ ├── infrastructure/
│ │ │ │ ├── database/
│ │ │ │ │ ├── models/
│ │ │ │ │ └── repositories/
│ │ │ │ ├── factories/
│ │ │ │ ├── jobs/
│ │ │ │ └── market-data/
│ │ │ └── presentation/
│ │ │ └── http/
│ │ │ ├── controllers/
│ │ │ ├── routes/
│ │ │ └── validators/
│ │ │
│ │ └── user/
│ │ ├── application/
│ │ │ ├── dto/
│ │ │ ├── factories/
│ │ │ └── use-cases/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── enums/
│ │ │ └── repositories/
│ │ ├── infrastructure/
│ │ │ ├── database/
│ │ │ │ ├── models/
│ │ │ │ └── repositories/
│ │ │ ├── factories/
│ │ │ └── security/
│ │ └── presentation/
│ │ └── http/
│ │ ├── controllers/
│ │ ├── routes/
│ │ └── validators/
│ │
│ ├── shared/
│ │ ├── application/
│ │ │ └── ports/
│ │ ├── config/
│ │ ├── infrastructure/
│ │ │ ├── database/
│ │ │ ├── factories/
│ │ │ └── security/
│ │ ├── presentation/
│ │ │ ├── middlewares/
│ │ │ ├── responses/
│ │ │ └── validators/
│ │ └── utils/
│ │
│ └── types/How a request should travel
A login request hits presentation/http/routes, passes validators, and enters a controller. The controller does not talk to MongoDB. It calls an application use case such as LoginUser, passing a DTO. The use case loads the user through a domain repository port, checks credentials via an application or infrastructure security port, and returns a result. Infrastructure supplies the concrete repository and token signer at composition time—factories wire those adapters once at startup. Controllers stay thin; use cases stay testable without spinning up Express.
Richer modules like backtesting follow the same path with extra room for domain services and strategies, infrastructure jobs, and market-data adapters. Heavy work belongs in workers under src/app/workers or module jobs, not inside HTTP handlers. That split is what keeps p99 healthy when strategy runs get long—see how to scale APIs with Node.js for the runtime side of the same philosophy.
Rules that keep the tree honest
Do not import infrastructure from domain. Do not put business rules in controllers. Do not let shared/utils become a junk drawer for domain logic that was uncomfortable to place. Prefer a new module when a concept has its own language and lifecycle; prefer shared only for truly cross-cutting concerns like config parsing, error envelopes, and auth middleware. Name folders after roles, not after libraries—database/repositories instead of mongoose/ so swapping an ORM does not rename your mental model.
- Dependency rule: domain ← application ← presentation / infrastructure
- One feature change should mostly touch one module’s four layers
- Compose adapters in factories; avoid new Repository() scattered in use cases
- Keep HTTP shapes in DTOs and validators—do not leak ORM documents to clients
- Put async and scheduled work in jobs/workers, not request handlers
- Document the tree in an ADR so new hires stop inventing parallel layouts
When to adopt this—and when to stay lean
A weekend MVP with two endpoints does not need every folder above. Start with modules and a light split—routes, use cases, repositories—then grow into full presentation and infrastructure trees as the team and surface area expand. Over-foldering a toy app creates ceremony without clarity. Under-structuring a multi-team SaaS creates merge conflicts and accidental coupling. Match structure to people and risk, not to a blog diagram alone.
From Kathmandu I help teams introduce this layout incrementally: extract one module, freeze imports with lint boundaries, then migrate the next bounded context. If your Node.js repo already feels tangled and you want a Senior Developer to sketch the cut lines, get in touch with a rough module list and your current pain (slow features, hard tests, or risky deploys). We can turn clean architecture from a buzzword into a folder tree your team can actually navigate.