NestJS Project Structure: The Exact Folder Setup We Ship

NestJS project structure is the decision that looks irrelevant in week one and runs your life by month six. The framework's CLI gives you a clean starting point, you start dropping controllers and services into it, and everything is fine — right up until it isn't. There's a fairly reliable moment, somewhere around the twentieth controller, when the "where does this file even go" conversation starts happening in every code review.
We've shipped enough NestJS backends to have hit that wall, refactored our way out of it, and settled on a structure we now use on every build. This is that structure — the exact folders, the three-layer module system, real AppModule and CoreModule code, and the rules that keep it from rotting as the team grows from three developers to ten.
The short answer: The best NestJS project structure groups code by feature, not by layer. Use three layers — a Core module for app-wide infrastructure (config, database, logging), a Shared module for reusable providers and DTOs, and Feature modules, one per business domain, each owning its own controller, service, DTOs, and tests. A top-level common/ folder holds generic decorators, filters, guards, and pipes.
What Goes Wrong With the Default NestJS Project Structure

Most tutorials teach a layered structure: one controllers/ folder, one services/ folder, one dtos/ folder, and so on. It demos beautifully. It also breaks at scale, and the breakage is predictable.
The problem is that nothing you actually work on lives in one place. Here's the layered version, which is what most projects drift into:
1src/
2├── controllers/ # billing, auth, users, projects... all 20 of them
3├── services/ # billing, auth, users, projects... all 20 again
4├── dtos/ # one subfolder per domain, far from its controller
5└── entities/ # and againA change to billing means opening controllers/billing.controller.ts, services/billing.service.ts, dtos/billing/, and entities/billing.entity.ts — four folders, scattered among forty other unrelated files. Multiply that across a team and you get constant merge conflicts in folders that have become dumping grounds, and a new hire who needs a week to find anything. Deleting a feature is worse: you're hunting its pieces across the whole tree and hoping you got them all.
There are over 30,000 SaaS companies competing for the same buyers (industry trackers, 2025), and developers already burn an estimated 33–42% of their time wrestling technical debt rather than shipping features (Stripe Developer Coefficient). A structure that fights you on every change is how that number climbs. The fix isn't clever — it's just organizing around the thing you actually change: a feature.
The NestJS Project Structure We Use in Production
Here's the layout, top to bottom. Three layers — Core, Shared, Feature — plus a common/ folder for framework-level generics.
1src/
2├── main.ts # bootstrap
3├── app.module.ts # root: imports Core, Shared, and all Feature modules
4│
5├── core/ # LAYER 1 — imported once, app-wide infrastructure
6│ ├── core.module.ts
7│ ├── config/ # env loading + validation
8│ ├── database/ # DataSource, connection
9│ └── logging/ # logger setup
10│
11├── shared/ # LAYER 2 — reusable across features
12│ ├── shared.module.ts
13│ ├── services/ # e.g. PaginationService, ClockService
14│ └── dto/ # shared response/pagination DTOs
15│
16├── modules/ # LAYER 3 — one folder per business domain
17│ ├── auth/
18│ │ ├── auth.module.ts
19│ │ ├── auth.controller.ts
20│ │ ├── auth.service.ts
21│ │ ├── dto/
22│ │ └── auth.service.spec.ts
23│ ├── billing/
24│ ├── projects/
25│ └── users/
26│
27└── common/ # framework generics, no business logic
28 ├── decorators/
29 ├── filters/
30 ├── guards/
31 ├── interceptors/
32 └── pipes/The mental model is simple. common/ is generic framework plumbing with no knowledge of your domain. core/ is infrastructure that must exist exactly once. shared/ is reusable business-adjacent code. modules/ is where your actual product lives, one bounded context per folder. If you can answer "which of these four does this file belong to," you always know where it goes — and so does everyone else.
Layer 1: The Core Module (Config, Database, Logging)
The Core module holds the infrastructure that should be initialized once for the whole app: configuration, the database connection, and logging. The root AppModule imports it exactly once, and feature modules never touch it.
1// core/core.module.ts
2import { Global, Module } from '@nestjs/common';
3@Global() // makes exported providers injectable everywhere without re-importing
4@Module({
5 imports: [
6 ConfigModule.forRoot({ isGlobal: true, validate: validateEnv }),
7 DatabaseModule,
8 LoggingModule,
9 ],
10 exports: [DatabaseModule, LoggingModule],
11})
12export class CoreModule {}1// app.module.ts
2import { Module } from '@nestjs/common';
3@Module({
4 imports: [
5 CoreModule, // infrastructure, once
6 SharedModule, // reusable providers
7 AuthModule, // feature modules
8 BillingModule,
9 ProjectsModule,
10 UsersModule,
11 ],
12})
13export class AppModule {}The @Global() decorator is the one shortcut worth taking here: it lets things like the logger and config be injected anywhere without every module re-importing them. Use it sparingly and only in Core — making everything global is how you lose track of what depends on what. The official NestJS modules docs cover the global-module mechanics in detail if you want the exact semantics.
Layer 2: The Shared Module (Reusable Services, DTOs, Guards)
Shared is for code that more than one feature needs but that isn't infrastructure. Pagination helpers, a common ApiResponse DTO, a date/clock service you mock in tests — that kind of thing. The rule that keeps Shared from becoming a junk drawer: if only one feature uses it, it doesn't belong here. It belongs in that feature. Shared earns its place only when the second feature needs the same thing.
1// shared/shared.module.ts
2import { Module } from '@nestjs/common';
3@Module({
4 providers: [PaginationService, ClockService],
5 exports: [PaginationService, ClockService],
6})
7export class SharedModule {}This is also where the difference between shared/ and common/ matters. common/ holds generic NestJS building blocks — a @CurrentUser() decorator, an HttpExceptionFilter, a ValidationPipe config — none of which know anything about your business. shared/ holds reusable code that does know a little about your domain. Keeping them separate stops your framework plumbing and your business helpers from tangling together.
Layer 3: Feature Modules (One Per Bounded Context)

This is the part that actually matters. Each feature is one folder under modules/, and it owns everything it needs: its controller, service, DTOs, entities, and tests, all in one place. One module per business domain — auth, billing, projects, users — mapped to bounded contexts, not to database tables.
The discipline that makes feature modules work is encapsulation through exports. A module should export only what other modules genuinely need to inject — usually its service, sometimes nothing at all.
1// modules/billing/billing.module.ts
2import { Module } from '@nestjs/common';
3@Module({
4 controllers: [BillingController],
5 providers: [BillingService, StripeWebhookHandler],
6 exports: [BillingService], // only the service is public; the webhook handler stays private
7})
8export class BillingModule {}Exporting everything "just in case" is how you end up with a graph where every module depends on every other module — which is the layered swamp again, just wearing module decorators. Export the minimum. The day another module needs more, you'll know, and you can decide deliberately.
Inside a feature, keep the providers honest too. The controller handles HTTP and nothing else; the service holds the business logic; anything that talks to a third party (a Stripe client, an email sender) gets its own injectable provider so you can mock it in tests. A feature module that has one 600-line service doing all three jobs isn't a module, it's a monolith with a folder around it. When the service gets fat, split it by responsibility within the feature before you even think about new modules.
Handling Cross-Module Dependencies Without Circular Imports

Sooner or later two feature modules want each other, and NestJS throws a circular-dependency error. The framework gives you forwardRef() to break the cycle:
1// Works — but treat the warning as a design smell, not a solution
2@Module({
3 imports: [forwardRef(() => UsersModule)],
4})
5export class AuthModule {}Here's the honest take: reaching for forwardRef() usually means your boundaries are wrong, not that NestJS is being difficult. When AuthModule and UsersModule both need each other, the shared piece almost always belongs in a third place — either a lower-level module both import, or the Shared layer. Fix the structure and the cycle disappears, no forwardRef() required. The NestJS circular dependency docs are clear that it's an escape hatch, not a pattern.
One sneaky cause worth calling out: barrel files. Using index.ts files to re-export your module and provider classes is convenient and is also a classic way to introduce circular imports that are miserable to trace. For modules and providers, import from the real file path and skip the barrel. (Yes, it's slightly less pretty. It's also slightly less likely to ruin a Tuesday.)
How This Structure Scales From 3 Developers to 10

The real payoff of feature modules shows up when the team grows. With a layered structure, three developers working on three different features all end up editing the same services/ folder, and you get merge conflicts in code that has nothing to do with each other. With feature modules, the billing developer lives in modules/billing/ and the auth developer lives in modules/auth/, and they almost never touch the same files.
That's not a small quality-of-life thing — it's the difference between a codebase where ten people can work in parallel and one where they're constantly stepping on each other. Module boundaries become team boundaries. When you eventually onboard someone, "you own the projects module" is a sentence that actually means something, because the module is a real, self-contained unit and not a slice spread across the whole tree.
This is also why I'll plant a flag here: you almost certainly don't need microservices for this. A well-structured modular monolith gives you the clean boundaries people think they're buying with microservices, without trading your function calls for network calls and a distributed-tracing bill you'll be debugging at 3am. Get the modules right inside one deployable. The day a single module genuinely needs to scale or deploy independently, it's already a clean unit you can lift out — but that day is later than most teams think, and often never.
7 Rules We Enforce in Code Review
The structure is only as good as the habits around it. These are the rules that actually keep a NestJS project structure clean once more than one person is committing to it.
- Group by feature, never by layer. A new feature is a new folder under
modules/, with everything it needs inside. - One bounded context per module. Map modules to business domains, not to database tables.
- Export the minimum. A module exposes only the providers others must inject. Default to exporting nothing.
- Core is imported once. Infrastructure lives in Core and the root module imports it. Feature modules never import Core.
- Earn your way into Shared. Code moves to Shared only when a second feature needs it — not preemptively.
- No business logic in
common/. It's framework generics only. The moment a filter knows about billing, it's in the wrong folder. - Fix cycles, don't
forwardRef()them. A circular dependency is a boundary problem. Solve it in the structure.
The Refactor: What Our Structure Looked Like Before and After

We didn't arrive at this by reading a style guide. On an early build we used the textbook layered structure, and it was genuinely fine for a while — small team, handful of controllers, everyone knew where everything was. Then the controller count crept up, two more developers joined, and the services/ folder turned into a place where merge conflicts went to breed. The "where does this go" question stopped having an obvious answer.
The refactor itself was unglamorous: we created modules/, and moved each domain's controller, service, and DTOs into its own folder, one feature at a time, shipping between each move. No big-bang rewrite, no week-long freeze — just incremental relocation while the app stayed green. (See our multi-tenant SaaS database guide for how we apply the same "decide the boundary early, migrate incrementally" instinct to the data layer.) The before-and-after wasn't about lines of code. It was that code review stopped including the phrase "actually, can you move that file."
Final Word on NestJS Project Structure
The structure that wins isn't the cleverest one — it's the one where every file has an obvious home and every developer agrees on where that is. Group by feature, keep Core for infrastructure, make modules earn their exports, and fix cycles instead of hiding them. Do that and your NestJS project structure stops being a thing you fight and starts being a thing you barely notice, which is exactly what good architecture feels like.
If you're staring at a services/ folder with forty files in it and wondering whether it's worth the untangling, it usually is — and it's the kind of thing we do for a living. Tell us about your backend and we'll talk through the migration, or read more engineering guides first. Either way, organize around what you change. Future-you, three developers and forty controllers from now, will be glad you did.
Frequently Asked Questions
Group by feature, not by technical layer. Use three layers: a Core module for app-wide infrastructure (config, database, logging) imported once, a Shared module for reusable providers, guards, and DTOs, and Feature modules — one per business domain (auth, billing, projects) — each holding its own controller, service, DTOs, and tests. A top-level common/ folder holds generic decorators, filters, and pipes. This scales far better than the default layered structure of separate controllers/, services/, and dtos/ folders.
By feature. A layered structure (all controllers in one folder, all services in another) looks tidy at five files and becomes painful past roughly 20 controllers, because every change forces you to jump across four folders. Feature-based organization keeps everything a domain needs in one folder, so adding a feature is creating one folder, and deleting one is deleting one folder.
Fix the structure rather than papering over it. Most circular dependencies come from two feature modules importing each other — usually a sign the shared logic belongs in a third module or the Shared layer. NestJS offers forwardRef() as an escape hatch, but treat its warning as a design smell. Also avoid using barrel (index.ts) files to re-export module and provider classes, which is a common hidden cause.
App-wide infrastructure that should exist exactly once: configuration (ConfigModule), the database connection, logging, and global guards or interceptors. The Core module is imported a single time by the root AppModule and never by feature modules. If you find a feature importing CoreModule, that's a sign something global leaked into a feature boundary.
For most early-stage SaaS, yes. A well-structured modular monolith gives you clean domain boundaries inside one deployable you can run on a laptop and debug in one place. Microservices solve an organizational problem — many teams deploying independently — not a traffic problem, and they trade function calls for network calls and a distributed-tracing bill. Get the module boundaries right first; you can split a clean module into a service later if a boundary genuinely demands it.
