NestJS Patterns Preset
The NestJS preset enforces NestJS architectural best practices, ensuring proper separation of concerns across controllers, services, repositories, and DTOs.
What is the NestJS Preset?
The NestJS preset enforces patterns specific to NestJS applications:
- Controllers handle HTTP only - no business logic or direct database access
- Services contain business logic - called by controllers
- Repositories manage data - called by services
- DTOs define API contracts - separate from entities
- Entities are for data models - used by repositories and services
- Cross-cutting concerns (guards, pipes, interceptors) available everywhere
The core principle: Controllers → Services → Repositories, with clear separation.
When to Use This Preset
Use the NestJS preset when you have:
- NestJS application (any version)
- API backend with clear layering needs
- Dependency injection patterns
- TypeScript decorators usage
- Need to enforce NestJS best practices
Architecture Diagram
%%{init: {'theme':'base', 'themeVariables': {
'primaryColor':'#b3d9ff',
'primaryBorderColor':'#1976d2',
'secondaryColor':'#b3ffcc',
'secondaryBorderColor':'#2e7d32',
'tertiaryColor':'#ffffb3',
'tertiaryBorderColor':'#f57c00'
}}}%%
graph TB
subgraph Controllers["📁 Controllers
HTTP Layer"]
UserCtrl["user.controller.ts"]
ProductCtrl["product.controller.ts"]
end
subgraph DTOs["📁 DTOs
API Contracts"]
CreateUserDto["create-user.dto.ts"]
UpdateUserDto["update-user.dto.ts"]
end
subgraph Services["📁 Services
Business Logic"]
UserSvc["user.service.ts"]
ProductSvc["product.service.ts"]
end
subgraph Entities["📁 Entities
Data Models"]
UserEntity["user.entity.ts"]
ProductEntity["product.entity.ts"]
end
subgraph Repositories["📁 Repositories
Data Access"]
UserRepo["user.repository.ts"]
ProductRepo["product.repository.ts"]
end
subgraph CrossCutting["📁 Common
Guards, Pipes, Interceptors"]
Guards["guards/"]
Pipes["pipes/"]
Interceptors["interceptors/"]
end
%% Allowed dependencies (green arrows)
UserCtrl --> UserSvc
UserCtrl --> CreateUserDto
ProductCtrl --> ProductSvc
UserSvc --> UserRepo
UserSvc --> UserEntity
UserSvc --> CreateUserDto
ProductSvc --> ProductRepo
UserRepo --> UserEntity
ProductRepo --> ProductEntity
UserCtrl --> Guards
UserSvc --> Guards
UserCtrl --> Pipes
style Controllers fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style DTOs fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style Services fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px
style Entities fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
style Repositories fill:#ffebee,stroke:#c62828,stroke-width:2px
style CrossCutting fill:#f5f5f5,stroke:#616161,stroke-width:2px
linkStyle default stroke:#22c55e,stroke-width:2px
Directory Structure
src/├── users/│ ├── users.module.ts # Module definition│ ├── users.controller.ts # HTTP controller│ ├── users.service.ts # Business logic│ ├── users.repository.ts # Data access│ ├── dto/│ │ ├── create-user.dto.ts # Input DTO│ │ └── update-user.dto.ts # Input DTO│ └── entities/│ └── user.entity.ts # Database entity├── products/│ ├── products.module.ts│ ├── products.controller.ts│ ├── products.service.ts│ └── entities/│ └── product.entity.ts├── common/ # Shared utilities│ ├── guards/│ │ └── auth.guard.ts│ ├── interceptors/│ │ └── logging.interceptor.ts│ ├── pipes/│ │ └── validation.pipe.ts│ └── decorators/│ └── user.decorator.ts├── config/ # Configuration│ └── database.config.ts└── main.ts # Application entry pointBoundaries Defined
The preset defines multiple boundaries:
1. Controllers
- Pattern:
src/**/*.controller.ts - Tags:
nestjs,controllers,presentation - Purpose: HTTP controllers handling requests and responses
- Layer: 0 (entry points)
2. Services
- Pattern:
src/**/*.service.ts - Tags:
nestjs,services,business-logic - Purpose: Business logic providers injected via DI
- Layer: 1
3. DTOs
- Pattern:
src/**/dto/** - Tags:
nestjs,dtos,contracts - Purpose: Data Transfer Objects for API contracts
- Layer: 0 (presentation layer)
4. Entities
- Pattern:
src/**/entities/** - Tags:
nestjs,entities,data - Purpose: Database entities and domain models
- Layer: 2 (data layer)
5. Repositories
- Pattern:
src/**/*.repository.ts - Tags:
nestjs,repositories,data - Purpose: Data access layer (TypeORM, Prisma, etc.)
- Layer: 2
6-11. Cross-Cutting Concerns
- Guards:
src/**/guards/** - Interceptors:
src/**/interceptors/** - Pipes:
src/**/pipes/** - Decorators:
src/**/decorators/** - Common:
src/common/** - Config:
src/config/** - Purpose: Available throughout the application
Key Rules Enforced
Controllers Call Services, Not Repositories
Controllers delegate to services for business logic, never accessing data directly.
// ❌ BAD - Controller using repository directlyimport { UsersRepository } from './users.repository'
@Controller('users')export class UsersController { constructor(private usersRepo: UsersRepository) {} // Violation!
@Get() findAll() { return this.usersRepo.findAll() // Skipping service layer! }}// ✅ GOOD - Controller using serviceimport { UsersService } from './users.service'
@Controller('users')export class UsersController { constructor(private usersService: UsersService) {}
@Get() findAll() { return this.usersService.findAll() }}DTOs Cannot Import Entities
DTOs define API contracts, entities define database models. Keep them separate.
// ❌ BAD - DTO importing entityimport { User } from '../entities/user.entity'
export class CreateUserDto extends User {} // Tight coupling!// ✅ GOOD - DTO independent from entityimport { IsEmail, IsString, MinLength } from 'class-validator'
export class CreateUserDto { @IsEmail() email: string
@IsString() @MinLength(2) name: string
@IsString() @MinLength(8) password: string}Controllers Cannot Import Entities
Controllers should use DTOs for API contracts, not entities.
// ❌ BAD - Controller using entityimport { User } from './entities/user.entity'
@Controller('users')export class UsersController { @Get() findAll(): Promise<User[]> { // Exposes database structure! return this.usersService.findAll() }}// ✅ GOOD - Controller using DTOimport { UserDto } from './dto/user.dto'
@Controller('users')export class UsersController { @Get() async findAll(): Promise<UserDto[]> { const users = await this.usersService.findAll() return users.map(u => new UserDto(u)) }}Controllers Are Independent
Controllers should not import each other. Share logic via services.
// ❌ BAD - Controller importing another controllerimport { UsersController } from '../users/users.controller'
@Controller('products')export class ProductsController { constructor(private usersCtrl: UsersController) {} // Violation!}// ✅ GOOD - Controllers using shared serviceimport { UsersService } from '../users/users.service'
@Controller('products')export class ProductsController { constructor(private usersService: UsersService) {} // Correct!}Repositories Work with Entities, Not DTOs
Repositories persist entities, not DTOs.
// ❌ BAD - Repository using DTOimport { CreateUserDto } from './dto/create-user.dto'
@Injectable()export class UsersRepository { async create(dto: CreateUserDto) {} // Wrong!}// ✅ GOOD - Repository using entityimport { User } from './entities/user.entity'
@Injectable()export class UsersRepository { constructor( @InjectRepository(User) private repo: Repository<User> ) {}
async create(user: User): Promise<User> { return this.repo.save(user) }
async findAll(): Promise<User[]> { return this.repo.find() }}Example Configuration
{ "preset": "@stricture/nestjs"}No additional configuration needed for standard NestJS structure.
Real Code Example
Controller
import { Controller, Get, Post, Body, Param, UseGuards} from '@nestjs/common'import { UsersService } from './users.service'import { CreateUserDto } from './dto/create-user.dto'import { UserDto } from './dto/user.dto'import { AuthGuard } from '../common/guards/auth.guard'
@Controller('users')@UseGuards(AuthGuard)export class UsersController { constructor(private readonly usersService: UsersService) {}
@Post() async create(@Body() createUserDto: CreateUserDto): Promise<UserDto> { const user = await this.usersService.create(createUserDto) return new UserDto(user) }
@Get() async findAll(): Promise<UserDto[]> { const users = await this.usersService.findAll() return users.map(u => new UserDto(u)) }
@Get(':id') async findOne(@Param('id') id: string): Promise<UserDto> { const user = await this.usersService.findOne(id) return new UserDto(user) }}Service
import { Injectable, NotFoundException } from '@nestjs/common'import { UsersRepository } from './users.repository'import { User } from './entities/user.entity'import { CreateUserDto } from './dto/create-user.dto'import * as bcrypt from 'bcrypt'
@Injectable()export class UsersService { constructor(private readonly usersRepo: UsersRepository) {}
async create(createUserDto: CreateUserDto): Promise<User> { // Business logic const existingUser = await this.usersRepo.findByEmail( createUserDto.email )
if (existingUser) { throw new ConflictException('User already exists') }
// Hash password const hashedPassword = await bcrypt.hash( createUserDto.password, 10 )
// Create entity const user = new User() user.email = createUserDto.email user.name = createUserDto.name user.password = hashedPassword
// Save via repository return this.usersRepo.create(user) }
async findAll(): Promise<User[]> { return this.usersRepo.findAll() }
async findOne(id: string): Promise<User> { const user = await this.usersRepo.findById(id) if (!user) { throw new NotFoundException(`User with ID ${id} not found`) } return user }}Repository
import { Injectable } from '@nestjs/common'import { InjectRepository } from '@nestjs/typeorm'import { Repository } from 'typeorm'import { User } from './entities/user.entity'
@Injectable()export class UsersRepository { constructor( @InjectRepository(User) private readonly repo: Repository<User> ) {}
async create(user: User): Promise<User> { return this.repo.save(user) }
async findAll(): Promise<User[]> { return this.repo.find() }
async findById(id: string): Promise<User | null> { return this.repo.findOne({ where: { id } }) }
async findByEmail(email: string): Promise<User | null> { return this.repo.findOne({ where: { email } }) }}DTO
import { IsEmail, IsString, MinLength } from 'class-validator'
export class CreateUserDto { @IsEmail() email: string
@IsString() @MinLength(2) name: string
@IsString() @MinLength(8) password: string}import { Exclude } from 'class-transformer'import { User } from '../entities/user.entity'
export class UserDto { id: string email: string name: string createdAt: Date
@Exclude() password: string
constructor(partial: Partial<User>) { Object.assign(this, partial) }}Entity
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn} from 'typeorm'
@Entity('users')export class User { @PrimaryGeneratedColumn('uuid') id: string
@Column({ unique: true }) email: string
@Column() name: string
@Column() password: string
@CreateDateColumn() createdAt: Date}Module
import { Module } from '@nestjs/common'import { TypeOrmModule } from '@nestjs/typeorm'import { UsersController } from './users.controller'import { UsersService } from './users.service'import { UsersRepository } from './users.repository'import { User } from './entities/user.entity'
@Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService, UsersRepository], exports: [UsersService] // Export for other modules})export class UsersModule {}Cross-Cutting Concern (Guard)
import { Injectable, CanActivate, ExecutionContext} from '@nestjs/common'import { Observable } from 'rxjs'
@Injectable()export class AuthGuard implements CanActivate { canActivate( context: ExecutionContext ): boolean | Promise<boolean> | Observable<boolean> { const request = context.switchToHttp().getRequest() const token = request.headers.authorization?.split(' ')[1]
if (!token) { return false }
// Validate token return this.validateToken(token) }
private async validateToken(token: string): Promise<boolean> { // Token validation logic return true }}Common Violations and Fixes
Violation: Controller bypassing service layer
// ❌ BAD@Controller('users')export class UsersController { constructor(private usersRepo: UsersRepository) {}
@Get() findAll() { return this.usersRepo.findAll() // Skipping service layer! }}Fix: Use service layer
// ✅ GOOD@Controller('users')export class UsersController { constructor(private usersService: UsersService) {}
@Get() findAll() { return this.usersService.findAll() }}Violation: DTO extending entity
// ❌ BADimport { User } from '../entities/user.entity'
export class CreateUserDto extends User { // This creates tight coupling!}Fix: Define DTOs independently
// ✅ GOODexport class CreateUserDto { @IsEmail() email: string
@IsString() name: string}
// Service maps between DTO and Entityasync create(dto: CreateUserDto): Promise<User> { const user = new User() user.email = dto.email user.name = dto.name return this.usersRepo.create(user)}Violation: Repository using DTO
// ❌ BAD@Injectable()export class UsersRepository { async create(dto: CreateUserDto) { // Repository should work with entities! }}Fix: Repository uses entities
// ✅ GOOD@Injectable()export class UsersRepository { async create(user: User): Promise<User> { return this.repo.save(user) }}
// Service maps DTO to Entityasync create(dto: CreateUserDto): Promise<User> { const user = this.mapDtoToEntity(dto) return this.usersRepo.create(user)}Benefits
- Clear separation of concerns: Each layer has a specific responsibility
- Testability: Easy to mock services and repositories
- Maintainability: Changes are localized to appropriate layers
- Type safety: DTOs provide compile-time validation
- Scalability: Pattern works well as application grows
Trade-offs
- More files: Need controllers, services, repositories, DTOs, entities
- Boilerplate: Mapping between DTOs and entities
- Learning curve: Understanding dependency injection and decorators
Best Practices
Keep Controllers Thin
Controllers should only handle HTTP concerns.
// ✅ GOOD - Thin controller@Controller('users')export class UsersController { constructor(private usersService: UsersService) {}
@Post() create(@Body() dto: CreateUserDto) { return this.usersService.create(dto) }}
// ❌ BAD - Fat controller@Controller('users')export class UsersController { @Post() async create(@Body() dto: CreateUserDto) { // Validation logic if (!dto.email.includes('@')) throw new Error() // Business logic const hashedPwd = await bcrypt.hash(dto.password, 10) // Database access return this.db.save({ ...dto, password: hashedPwd }) // Too much responsibility! }}Use DTOs for All API Boundaries
// Input DTOs for requestsexport class CreateUserDto { ... }
// Output DTOs for responsesexport class UserDto { ... }
// Don't expose entities directly// ❌ @Get() findAll(): Promise<User[]>// ✅ @Get() findAll(): Promise<UserDto[]>Export Services, Not Repositories
// ✅ GOOD - Module exports service@Module({ exports: [UsersService]})export class UsersModule {}
// ❌ BAD - Module exports repository@Module({ exports: [UsersRepository] // Should be internal to module})Related Patterns
- Layered Architecture - Similar layering approach
- Hexagonal Architecture - Alternative with ports/adapters
Next Steps
- Check out the NestJS example
- Read NestJS documentation
- Learn about NestJS fundamentals