Project Structure
When you scaffold a project with nest new, the CLI hands you a small, deliberate directory layout rather than an empty folder. Every file in that tree exists for a reason, and understanding what each one does — from the main.ts entry point to the root AppModule — is the fastest way to feel at home in NestJS. This page walks through the generated structure, explains how the application boots, and lays out the conventions Nest teams follow to keep large codebases organized.
The scaffolded directory tree
A freshly generated NestJS project looks like this:
my-app/
├── src/
│ ├── app.controller.ts
│ ├── app.controller.spec.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test/
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── nest-cli.json
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── eslint.config.mjs
└── .prettierrc
Everything your application runs lives under src/. The test/ folder holds end-to-end specs (unit .spec.ts files sit next to the code they test). The remaining root files are configuration: nest-cli.json drives the CLI and build, tsconfig.json configures TypeScript, and package.json defines scripts and dependencies.
| File / folder | Purpose |
|---|---|
src/main.ts | Application entry point that bootstraps Nest |
src/app.module.ts | Root module that wires the app together |
src/app.controller.ts | Example controller handling HTTP routes |
src/app.service.ts | Example provider holding business logic |
nest-cli.json | CLI and compiler configuration |
tsconfig.json | TypeScript compiler options |
test/ | End-to-end (e2e) test suite |
The bootstrap: main.ts
main.ts is where the application starts. It creates an application instance from the root module and tells it to listen on a port. This is the one file in a Nest app that imperatively kicks everything off.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
NestFactory.create() takes the root module and builds the full dependency-injection container, instantiating every controller and provider declared in the module graph. The returned app object is where you apply application-wide concerns before listening — global pipes, CORS, prefixes, and more.
// src/main.ts (with common bootstrap setup)
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableCors();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Output:
[Nest] 12480 - 06/14/2026, 9:41:02 AM LOG [NestFactory] Starting Nest application...
[Nest] 12480 - 06/14/2026, 9:41:02 AM LOG [InstanceLoader] AppModule dependencies initialized
[Nest] 12480 - 06/14/2026, 9:41:02 AM LOG [RoutesResolver] AppController {/api}
[Nest] 12480 - 06/14/2026, 9:41:02 AM LOG [RouterExplorer] Mapped {/api, GET} route
[Nest] 12480 - 06/14/2026, 9:41:02 AM LOG [NestApplication] Nest application successfully started
Tip: Keep
main.tslean. It should bootstrap and configure the app, not contain business logic. Anything reusable belongs in a module or provider so it stays inside the DI container.
The root AppModule
Every Nest application has exactly one root module, conventionally AppModule. The @Module() decorator describes the application graph — which controllers serve requests, which providers can be injected, and which other modules this one depends on.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
The four metadata keys define the module’s surface area:
| Key | Meaning |
|---|---|
imports | Other modules whose exported providers this module needs |
controllers | Controllers instantiated and route-mapped by this module |
providers | Injectable classes available within this module |
exports | Subset of providers made visible to importing modules |
As the app grows, the root module’s job shifts from holding controllers to importing feature modules — imports: [UsersModule, AuthModule, ...] — keeping each domain self-contained.
Controllers and services
The example AppController and AppService demonstrate the core split Nest enforces everywhere: controllers handle the HTTP layer, services hold the logic.
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
// src/app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
The controller never builds its service with new — it declares the dependency in the constructor and the DI container injects the singleton instance. This separation is the foundation for testing, swapping implementations, and scaling the codebase.
Conventions for organizing features
Beyond the scaffold, Nest projects follow a strong feature-module convention: group everything that belongs to one domain into its own folder, with files named by responsibility.
src/
├── users/
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── users.module.ts
│ └── users.controller.spec.ts
├── auth/
│ └── ...
├── common/ # shared guards, filters, interceptors, decorators
├── config/ # configuration and environment loading
├── app.module.ts
└── main.ts
The naming pattern — name.role.ts (users.controller.ts, users.service.ts) — is how the Nest CLI generates files and how every Nest developer recognizes them at a glance. Cross-cutting code that is not tied to one feature lives in common/ (guards, filters, interceptors, custom decorators), and environment/config loading lives in config/.
Static assets — files served directly such as images, downloads, or a built frontend — are typically placed in a top-level public/ or client/ folder and served with ServeStaticModule from @nestjs/serve-static, keeping them out of the compiled src/ tree.
Best Practices
- Organize by feature module, not by technical layer — colocate a domain’s controller, service, module, DTOs, and entities in one folder.
- Follow the
name.role.tsnaming convention so the CLI and your teammates can navigate the project predictably. - Keep
main.tsfocused on bootstrap and global configuration; move all logic into providers inside modules. - Let the root
AppModuleimport feature modules rather than directly declaring application controllers as the app grows. - Put cross-cutting concerns (guards, filters, interceptors) under
common/and configuration underconfig/. - Serve static assets from a dedicated
public/folder viaServeStaticModuleinstead of mixing them intosrc/. - Keep unit tests (
*.spec.ts) next to their source files and reservetest/for end-to-end specs.