Skip to content
NestJS best practices 4 min read

Project Structure Conventions

A NestJS application starts as a handful of files, but the structure you commit to in week one decides how painful month eighteen will be. The framework is deliberately unopinionated about folders, which means the discipline is yours to enforce. This page lays out a folder layout, layering model, and naming conventions that scale from a single team to dozens of developers without turning into a tangle of circular imports.

Organize by feature, not by type

The default Nest generator and many tutorials group code by technical role — one controllers/ folder, one services/ folder, one dtos/ folder. This collapses quickly: a single change to the “orders” feature forces you to touch four sibling directories, and unrelated features live next to each other for no reason. Instead, group by feature module, where everything a slice of the domain needs lives together.

src/
├── main.ts
├── app.module.ts
├── common/                  # cross-cutting: filters, guards, pipes, decorators
│   ├── filters/
│   ├── guards/
│   └── decorators/
├── config/                  # configuration schema + loaders
└── modules/
    ├── users/
    │   ├── users.module.ts
    │   ├── users.controller.ts
    │   ├── users.service.ts
    │   ├── users.repository.ts
    │   ├── dto/
    │   │   ├── create-user.dto.ts
    │   │   └── update-user.dto.ts
    │   └── entities/
    │       └── user.entity.ts
    └── orders/
        ├── orders.module.ts
        ├── orders.controller.ts
        └── ...

Each feature is a self-contained @Module. It exports only what other modules legitimately need and keeps everything else private.

// modules/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';

@Module({
  controllers: [UsersController],
  providers: [UsersService, UsersRepository],
  exports: [UsersService], // only the service crosses the module boundary
})
export class UsersModule {}

Export the narrowest surface possible. If OrdersModule imports UsersModule, it should reach UsersService, never UsersRepository. Leaking repositories couples features to each other’s persistence details.

Layer inside each feature

Within a feature, keep three layers with a strict dependency direction: the domain (entities, value objects, business rules) knows nothing about HTTP or the database; the application layer (services, use cases) orchestrates the domain; and the infrastructure layer (controllers, repositories, gateways) talks to the outside world. Dependencies always point inward.

// modules/users/users.service.ts  (application layer)
import { Injectable, ConflictException } from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(private readonly repo: UsersRepository) {}

  async create(dto: CreateUserDto): Promise<User> {
    if (await this.repo.findByEmail(dto.email)) {
      throw new ConflictException('Email already in use');
    }
    return this.repo.save(User.fromDto(dto));
  }
}

The controller stays thin — it maps HTTP to a use case and nothing more:

// modules/users/users.controller.ts  (infrastructure layer)
import { Body, Controller, Post } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.users.create(dto);
  }
}

This separation is what makes services unit-testable without spinning up Nest’s HTTP stack, and it lets you swap a TypeORM repository for a Prisma one without touching business logic.

Naming conventions

Consistency removes a class of cognitive load. Nest’s own CLI follows these patterns, so adopting them means generated code blends in seamlessly.

ArtifactFile suffixClass suffixExample
Module.module.tsModuleusers.module.ts
Controller.controller.tsControllerusers.controller.ts
Service.service.tsServiceusers.service.ts
Repository.repository.tsRepositoryusers.repository.ts
DTO.dto.tsDtocreate-user.dto.ts
Entity.entity.ts(domain name)user.entity.ts
Guard.guard.tsGuardroles.guard.ts
Filter.filter.tsFilterhttp-exception.filter.ts

Use kebab-case for file names and PascalCase for classes. Keep directory names plural for collections of features (modules/users) and singular for technical buckets (config, common).

Use barrel files at boundaries, not internally

A barrel file (index.ts re-exporting a folder’s public symbols) gives consumers one tidy import path. They are valuable at a feature’s public edge but become a circular-dependency trap when used between files inside the same module.

// modules/users/dto/index.ts  — a sensible barrel at a boundary
export * from './create-user.dto';
export * from './update-user.dto';
// consumer
import { CreateUserDto, UpdateUserDto } from './dto';

Avoid importing a sibling through a barrel that also re-exports that sibling — Nest resolves providers at bootstrap, and a barrel cycle can surface as a confusing undefined dependency at runtime rather than a compile error.

Output:

[Nest] ERROR [ExceptionHandler] Nest can't resolve dependencies of the UsersService (?).
Please make sure that the argument UsersRepository at index [0] is available.

That message almost always means a circular import through a barrel — break the cycle by importing the concrete file directly.

Best Practices

  • Group code by feature module first; reach for technical folders (common, config) only for genuinely cross-cutting concerns.
  • Keep a strict inward dependency direction: domain → application → infrastructure, never the reverse.
  • Export the minimum from each module; never let another feature import your repositories or entities directly.
  • Follow the CLI’s kebab-case file and .role.ts suffix conventions so generated and hand-written code look identical.
  • Use barrel files at module boundaries for clean imports, but avoid them between files inside the same module to prevent DI cycles.
  • Keep controllers thin — map HTTP to a use case and return; put all branching and rules in services and the domain.
  • Add a tsconfig path alias (for example @modules/*) so imports stay stable when you move folders.
Last updated June 14, 2026
Was this helpful?