Skip to content
NestJS ns getting-started 5 min read

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 / folderPurpose
src/main.tsApplication entry point that bootstraps Nest
src/app.module.tsRoot module that wires the app together
src/app.controller.tsExample controller handling HTTP routes
src/app.service.tsExample provider holding business logic
nest-cli.jsonCLI and compiler configuration
tsconfig.jsonTypeScript 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.ts lean. 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:

KeyMeaning
importsOther modules whose exported providers this module needs
controllersControllers instantiated and route-mapped by this module
providersInjectable classes available within this module
exportsSubset of providers made visible to importing modules

As the app grows, the root module’s job shifts from holding controllers to importing feature modulesimports: [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.ts naming convention so the CLI and your teammates can navigate the project predictably.
  • Keep main.ts focused on bootstrap and global configuration; move all logic into providers inside modules.
  • Let the root AppModule import feature modules rather than directly declaring application controllers as the app grows.
  • Put cross-cutting concerns (guards, filters, interceptors) under common/ and configuration under config/.
  • Serve static assets from a dedicated public/ folder via ServeStaticModule instead of mixing them into src/.
  • Keep unit tests (*.spec.ts) next to their source files and reserve test/ for end-to-end specs.
Last updated June 14, 2026
Was this helpful?