NestJS Code Style Guide
This document serves as the definitive guide for code style, patterns, and conventions used in the Awesome NestJS Boilerplate. This guide should be used as context for all future code generation and development work.
Table of Contents
- NestJS Code Style Guide
Project Overview
This is a modern NestJS boilerplate built with:
- NestJS 11.x - Progressive Node.js framework
- TypeScript 5.x - Strict type checking enabled
- TypeORM 0.3.x - Database ORM with PostgreSQL
- CQRS Pattern - Command Query Responsibility Segregation
- JWT Authentication - JSON Web Token based auth
- Swagger/OpenAPI - API documentation
- ESM Modules - ES Module support with .ts extensions
- Vite - Fast development server
- Jest - Testing framework
General TypeScript Guidelines
Basic Principles
- Use English for all code and documentation
- Always declare types for variables and functions (avoid
any) - Use strict TypeScript configuration
- One export per file
- No blank lines within functions
- No comments in code (self-documenting code preferred)
Naming Conventions
// PascalCase for classes
export class UserService {}
export class CreateUserDto {}
// camelCase for variables, functions, methods
const userName = 'john';
async function createUser() {}
// kebab-case for files and directories
user.service.ts
create-user.dto.ts
user-settings.entity.ts
// UPPERCASE for constants and environment variables
const MAX_RETRY_ATTEMPTS = 3;
process.env.DATABASE_URL
// Use verbs for boolean variables
const isLoading = true;
const hasError = false;
const canDelete = user.role === RoleType.ADMIN;Import Organization
// 1. Node modules (external dependencies)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
// 2. Internal imports with absolute paths and .ts extensions
import { validateHash } from '../../common/utils.ts';
import type { RoleType } from '../../constants/role-type.ts';
import { UserNotFoundException } from '../../exceptions/user-not-found.exception.ts';
// 3. Relative imports
import type { UserEntity } from './user.entity.ts';
import { UserService } from './user.service.ts';TypeScript Conventions
// Use type imports for types only
import type { RoleType } from '../constants/role-type.ts';
import type { Reference } from '../types.ts';
// Use readonly for input DTO properties
export class UserLoginDto {
@EmailField()
readonly email!: string;
@StringField()
readonly password!: string;
}
// Use definite assignment assertion for decorated properties
@Column({ unique: true })
email!: string;
// Use optional properties appropriately
phone?: string;File and Directory Structure
Project Structure
src/
├── main.ts # Application bootstrap
├── app.module.ts # Root module
├── setup-swagger.ts # Swagger configuration
├── types.ts # Global type definitions
├── boilerplate.polyfill.ts # Global polyfills
├── snake-naming.strategy.ts # Database naming strategy
├── metadata.ts # Application metadata
├── common/ # Shared utilities and base classes
│ ├── dto/ # Common DTOs
│ └── abstract.entity.ts # Base entity class
├── constants/ # Application constants
├── database/ # Database configuration and migrations
│ └── migrations/
├── decorators/ # Custom decorators
├── entity-subscribers/ # TypeORM entity subscribers
├── exceptions/ # Custom exception classes
├── filters/ # Exception filters
├── guards/ # Authentication/authorization guards
├── i18n/ # Internationalization files
│ ├── en_US/
│ └── ru_RU/
├── interceptors/ # Request/response interceptors
├── interfaces/ # TypeScript interfaces
├── modules/ # Feature modules
│ ├── auth/ # Authentication module
│ ├── user/ # User management module
│ ├── post/ # Post management module
│ └── health-checker/ # Health check module
├── providers/ # Custom providers
├── shared/ # Shared services and modules
│ └── services/
└── validators/ # Custom validatorsModule Structure
Each feature module follows this structure:
modules/feature-name/
├── commands/ # CQRS command handlers
│ ├── create-feature.command.ts
│ └── create-feature.handler.ts
├── queries/ # CQRS query handlers
│ ├── get-feature.query.ts
│ └── get-feature.handler.ts
├── dtos/ # Data Transfer Objects
│ ├── create-feature.dto.ts
│ ├── feature.dto.ts
│ └── features-page-options.dto.ts
├── exceptions/ # Module-specific exceptions
│ └── feature-not-found.exception.ts
├── feature.controller.ts # REST API controller
├── feature.service.ts # Business logic service
├── feature.module.ts # Module definition
├── feature.entity.ts # Database entity
└── feature-related.entity.ts # Related entitiesFile Naming Conventions
- Use kebab-case:
user-login.dto.ts - Include type in filename:
.controller.ts,.service.ts,.dto.ts,.entity.ts - Use singular names for entities:
user.entity.tsnotusers.entity.ts - Use descriptive names:
create-user.command.ts,user-not-found.exception.ts
Module Architecture
Module Definition Pattern
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CreateUserHandler } from './commands/create-user.handler.ts';
import { GetUserHandler } from './queries/get-user.handler.ts';
import { UserController } from './user.controller.ts';
import { UserEntity } from './user.entity.ts';
import { UserService } from './user.service.ts';
import { UserSettingsEntity } from './user-settings.entity.ts';
const handlers = [CreateUserHandler, GetUserHandler];
@Module({
imports: [
CqrsModule,
TypeOrmModule.forFeature([UserEntity, UserSettingsEntity]),
],
controllers: [UserController],
providers: [UserService, ...handlers],
exports: [UserService],
})
export class UserModule {}Controllers
Controller Structure Pattern
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Query,
ValidationPipe,
} from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { PageDto } from '../../common/dto/page.dto.ts';
import { RoleType } from '../../constants/role-type.ts';
import { ApiPageResponse } from '../../decorators/api-page-response.decorator.ts';
import { AuthUser } from '../../decorators/auth-user.decorator.ts';
import { Auth, UUIDParam } from '../../decorators/http.decorators.ts';
import { UserDto } from './dtos/user.dto.ts';
import { UsersPageOptionsDto } from './dtos/users-page-options.dto.ts';
import { UserEntity } from './user.entity.ts';
import { UserService } from './user.service.ts';
@Controller('users')
@ApiTags('users')
export class UserController {
constructor(
private userService: UserService,
private translationService: TranslationService,
) {}
@Post()
@HttpCode(HttpStatus.CREATED)
@Auth([RoleType.ADMIN])
@ApiOkResponse({ type: UserDto, description: 'User created successfully' })
async createUser(
@Body() createUserDto: CreateUserDto,
): Promise<UserDto> {
return this.userService.createUser(createUserDto);
}
@Get(':id')
@HttpCode(HttpStatus.OK)
@Auth([RoleType.USER])
@ApiOkResponse({ type: UserDto, description: 'Get user by ID' })
async getUser(@UUIDParam('id') userId: Uuid): Promise<UserDto> {
return this.userService.getUser(userId);
}
@Get()
@HttpCode(HttpStatus.OK)
@Auth([RoleType.USER])
@ApiPageResponse({
description: 'Get paginated users list',
type: PageDto,
})
async getUsers(
@Query(new ValidationPipe({ transform: true }))
pageOptionsDto: UsersPageOptionsDto,
): Promise<PageDto<UserDto>> {
return this.userService.getUsers(pageOptionsDto);
}
}Controller Best Practices
- Use dependency injection via constructor
- Mark injected services as
private - Use specific HTTP status codes with
@HttpCode() - Apply authentication/authorization with
@Auth()decorator - Use comprehensive Swagger documentation
- Use ValidationPipe for query parameters
- Use custom decorators like
@UUIDParam()for parameter validation - Keep controllers thin - delegate business logic to services
- Use versioning when needed with
@Version() - Return DTOs, not entities
- 🚨 CRITICAL: Follow REST API standards strictly for all endpoints
REST API Standards
All controllers MUST follow these REST API conventions:
HTTP Methods & Status Codes:
// Resource Creation
@Post() // 201 Created
@HttpCode(HttpStatus.CREATED)
// Resource Retrieval
@Get() // 200 OK (list)
@Get(':id') // 200 OK (single)
@HttpCode(HttpStatus.OK)
// Resource Update
@Put(':id') // 200 OK (full update)
@Patch(':id') // 200 OK (partial update)
@HttpCode(HttpStatus.OK)
// Resource Deletion
@Delete(':id') // 202 Accepted
@HttpCode(HttpStatus.ACCEPTED)URL Naming Conventions:
// ✅ CORRECT: Use plural nouns
@Controller('users') // /users
@Controller('posts') // /posts
@Controller('categories') // /categories
// ❌ WRONG: Don't use verbs or singular
@Controller('user') // ❌ singular
@Controller('getUsers') // ❌ verb in URL
@Controller('userManagement') // ❌ management suffixEndpoint Patterns:
// ✅ CORRECT REST endpoints
GET /users // List all users
POST /users // Create new user
GET /users/{id} // Get specific user
PUT /users/{id} // Update entire user
PATCH /users/{id} // Partially update user
DELETE /users/{id} // Delete user
// Nested resources
GET /users/{id}/posts // Get user's posts
POST /users/{id}/posts // Create post for user
// ❌ WRONG: Non-RESTful endpoints
POST /users/create // ❌ verb in URL
GET /users/list // ❌ unnecessary action
POST /deleteUser // ❌ wrong method + verb
GET /users/getById/{id} // ❌ unnecessary actionQuery Parameters for Filtering & Pagination:
// ✅ CORRECT: Use query parameters
GET /users?page=1&limit=10&role=admin&search=john
@Get()
async getUsers(
@Query() pageOptions: UsersPageOptionsDto,
): Promise<PageDto<UserDto>> {
return this.userService.getUsers(pageOptions);
}Response Format Standards:
// ✅ CORRECT: Consistent response structure
// Single resource
{
"id": "uuid",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com"
}
// List with pagination
{
"data": [...],
"meta": {
"page": 1,
"take": 10,
"itemCount": 50,
"pageCount": 5,
"hasPreviousPage": false,
"hasNextPage": true
}
}
// Error responses
{
"statusCode": 404,
"message": "User not found",
"error": "Not Found"
}Services
Service Structure Pattern
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PageDto } from '../../common/dto/page.dto.ts';
import { UserNotFoundException } from '../../exceptions/user-not-found.exception.ts';
import type { IFile } from '../../interfaces/IFile.ts';
import type { Reference } from '../../types.ts';
import { CreateUserDto } from './dtos/create-user.dto.ts';
import { UserDto } from './dtos/user.dto.ts';
import { UsersPageOptionsDto } from './dtos/users-page-options.dto.ts';
import { UserEntity } from './user.entity.ts';
@Injectable()
export class UserService {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
async createUser(
createUserDto: CreateUserDto,
file?: Reference<IFile>,
): Promise<UserEntity> {
const userEntity = this.userRepository.create(createUserDto);
if (file) {
this.userRepository.merge(userEntity, { avatar: file.key });
}
await this.userRepository.save(userEntity);
return userEntity;
}
async findOne(findOptions: Partial<UserEntity>): Promise<UserEntity> {
const userEntity = await this.userRepository
.createQueryBuilder('user')
.where('user.id = :id', { id: findOptions.id })
.getOne();
if (!userEntity) {
throw new UserNotFoundException();
}
return userEntity;
}
async getUser(userId: Uuid): Promise<UserDto> {
const userEntity = await this.findOne({ id: userId });
return userEntity.toDto();
}
async getUsers(
pageOptionsDto: UsersPageOptionsDto,
): Promise<PageDto<UserDto>> {
const queryBuilder = this.userRepository.createQueryBuilder('user');
const [items, pageMetaDto] = await queryBuilder.paginate(pageOptionsDto);
return items.toPageDto(pageMetaDto);
}
}Service Best Practices
- Use
@Injectable()decorator - Inject repositories with
@InjectRepository() - Mark dependencies as
private - Create separate methods for finding entities vs returning DTOs
- Throw custom exceptions for not found cases
- Use TypeORM query builders for complex queries
- Return DTOs from public methods, entities from private/internal methods
- Use pagination for list endpoints
- Handle transactions when needed
DTOs and Validation
Input DTO Pattern
import {
EmailField,
PasswordField,
PhoneFieldOptional,
StringField,
} from '../../../decorators/field.decorators.ts';
export class CreateUserDto {
@StringField()
firstName!: string;
@StringField()
lastName!: string;
@EmailField()
email!: string;
@PasswordField({ minLength: 6 })
password!: string;
@PhoneFieldOptional()
phone?: string;
}Response DTO Pattern
import { AbstractDto } from '../../../common/dto/abstract.dto.ts';
import { RoleType } from '../../../constants/role-type.ts';
import {
BooleanFieldOptional,
EmailFieldOptional,
EnumFieldOptional,
PhoneFieldOptional,
StringFieldOptional,
} from '../../../decorators/field.decorators.ts';
import type { UserEntity } from '../user.entity.ts';
export type UserDtoOptions = Partial<{ isActive: boolean }>;
export class UserDto extends AbstractDto {
@StringFieldOptional({ nullable: true })
firstName?: string | null;
@StringFieldOptional({ nullable: true })
lastName?: string | null;
@EnumFieldOptional(() => RoleType)
role?: RoleType;
@EmailFieldOptional({ nullable: true })
email?: string | null;
@PhoneFieldOptional({ nullable: true })
phone?: string | null;
@BooleanFieldOptional()
isActive?: boolean;
constructor(user: UserEntity, options?: UserDtoOptions) {
super(user);
this.firstName = user.firstName;
this.lastName = user.lastName;
this.role = user.role;
this.email = user.email;
this.phone = user.phone;
this.isActive = options?.isActive;
}
}Custom Field Decorators
The project uses custom field decorators that combine validation and Swagger documentation:
// String fields
@StringField() // Required string, min length 1
@StringFieldOptional() // Optional string
@StringField({ minLength: 3, maxLength: 50 }) // With constraints
// Email fields
@EmailField() // Required email
@EmailFieldOptional() // Optional email
// Password fields
@PasswordField({ minLength: 6 }) // Required password
@PasswordFieldOptional() // Optional password
// Phone fields
@PhoneField() // Required phone
@PhoneFieldOptional() // Optional phone
// Boolean fields
@BooleanField() // Required boolean
@BooleanFieldOptional() // Optional boolean
// Enum fields
@EnumField(() => RoleType) // Required enum
@EnumFieldOptional(() => RoleType) // Optional enum
// UUID fields
@UUIDField() // Required UUID
@UUIDFieldOptional() // Optional UUID
// Number fields
@NumberField() // Required number
@NumberFieldOptional() // Optional number
@NumberField({ min: 0, max: 100, int: true }) // With constraints
// Date fields
@DateField() // Required date
@DateFieldOptional() // Optional date
// URL fields
@URLField() // Required URL
@URLFieldOptional() // Optional URL
// Class fields
@ClassField(() => UserDto) // Required class field
@ClassField(() => TokenPayloadDto) // Required class field
// Enum fields
@EnumField(() => RoleType) // Required enum
@EnumFieldOptional(() => RoleType) // Optional enum
// Array Fields
@StringField({ each: true })
@StringFieldOptional({ each: true })
@StringField({ each: true, minLength: 3, maxLength: 50 })
@StringFieldOptional({ each: true, minLength: 3, maxLength: 50 })
@ClassField(() => UserDto, { each: true })
@ClassFieldOptional(() => TokenPayloadDto, { each: true })
@EnumField(() => RoleType, { each: true })
@EnumFieldOptional(() => RoleType, { each: true })
@NumberField({ each: true })
@NumberFieldOptional({ each: true })
@NumberField({ each: true, min: 0, max: 100, int: true })
@NumberFieldOptional({ each: true, min: 0, max: 100, int: true })
@DateField({ each: true })DTO Best Practices
- Use
readonlyfor input DTO properties; do NOT usereadonlyfor response DTO properties - Extend
AbstractDtofor response DTOs - Use custom field decorators for validation and Swagger documentation
- Use optional fields with proper typing (
?and| null) - Implement constructor for response DTOs that maps from entities
- Use options type for additional DTO construction parameters
- Use definite assignment assertion (
!) for required fields
Entities
Entity Structure Pattern
import { Column, Entity, OneToMany, OneToOne, VirtualColumn } from 'typeorm';
import { AbstractEntity } from '../../common/abstract.entity.ts';
import { RoleType } from '../../constants/role-type.ts';
import { UseDto } from '../../decorators/use-dto.decorator.ts';
import { PostEntity } from '../post/post.entity.ts';
import type { UserDtoOptions } from './dtos/user.dto.ts';
import { UserDto } from './dtos/user.dto.ts';
import { UserSettingsEntity } from './user-settings.entity.ts';
@Entity({ name: 'users' })
@UseDto(UserDto)
export class UserEntity extends AbstractEntity<UserDto, UserDtoOptions> {
@Column({ nullable: true, type: 'varchar' })
firstName!: string | null;
@Column({ nullable: true, type: 'varchar' })
lastName!: string | null;
@Column({ type: 'enum', enum: RoleType, default: RoleType.USER })
role!: RoleType;
@Column({ unique: true, nullable: true, type: 'varchar' })
email!: string | null;
@Column({ nullable: true, type: 'varchar' })
password!: string | null;
@Column({ nullable: true, type: 'varchar' })
phone!: string | null;
@Column({ nullable: true, type: 'varchar' })
avatar!: string | null;
@VirtualColumn({
query: (alias) =>
`SELECT CONCAT(${alias}.first_name, ' ', ${alias}.last_name)`,
})
fullName!: string;
@OneToOne(() => UserSettingsEntity, (userSettings) => userSettings.user)
settings?: UserSettingsEntity;
@OneToMany(() => PostEntity, (postEntity) => postEntity.user)
posts?: PostEntity[];
}Entity Best Practices
- Extend
AbstractEntityfor common fields (id, createdAt, updatedAt) - Use
@UseDto()decorator to link entity with its DTO - Use explicit column types:
type: 'varchar',type: 'enum' - Use definite assignment assertion (
!) for all properties - Use proper nullable typing:
string | nullfor nullable columns - Use
@VirtualColumnfor computed fields - Define relationships with proper decorators
- Use table names in plural:
{ name: 'users' } - 🚨 CRITICAL: Each entity must belong to only ONE module - do not share entities across multiple modules
Entity Module Ownership Rules
- One entity per module: Every entity must be owned by exactly one feature module
- No cross-module entity sharing: Do not import or use entities from other modules
- Use services for cross-module communication: If you need data from another module, use their service methods
- Create dedicated entities: If similar data structures are needed, create separate entities per module
- Exception for shared entities: Only
AbstractEntityand entities insrc/common/can be shared
Example of correct entity organization:
modules/user/
├── user.entity.ts ✅ Owned by user module
└── user-settings.entity.ts ✅ Owned by user module
modules/post/
├── post.entity.ts ✅ Owned by post module
└── post-category.entity.ts ✅ Owned by post module
// ❌ WRONG: Don't do this
modules/post/
├── post.entity.ts
└── user.entity.ts ❌ User entity belongs to user module
// ✅ CORRECT: Use services for cross-module data
@Injectable()
export class PostService {
constructor(
private userService: UserService, // ✅ Use service, not entity
) {}
}CQRS Pattern
Command Structure
// Command definition
import { Command } from '@nestjs/cqrs';
import type { CreatePostDto } from '../dtos/create-post.dto.ts';
export class CreatePostCommand extends Command {
constructor(
public userId: Uuid,
public createPostDto: CreatePostDto,
) {
super();
}
}Command Handler Structure
// Command handler
import type { ICommandHandler } from '@nestjs/cqrs';
import { CommandHandler } from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PostEntity } from '../post.entity.ts';
import { CreatePostCommand } from './create-post.command.ts';
import { PostDto } from '../dtos/post.dto.ts';
@CommandHandler(CreatePostCommand)
export class CreatePostHandler
implements ICommandHandler<CreatePostCommand>
{
constructor(
@InjectRepository(PostEntity)
private postRepository: Repository<PostEntity>,
) {}
async execute(command: CreatePostCommand): Promise<PostDto> {
const { userId, createPostDto } = command;
const postEntity = this.postRepository.create({ userId });
await this.postRepository.save(postEntity);
return postEntity.toDto();
}
}Query Structure
// Query definition
import type { IQuery } from '@nestjs/cqrs';
export class GetUserQuery implements IQuery {
constructor(public userId: Uuid) {}
}
// Query handler
import type { IQueryHandler } from '@nestjs/cqrs';
import { QueryHandler } from '@nestjs/cqrs';
@QueryHandler(GetUserQuery)
export class GetUserHandler implements IQueryHandler<GetUserQuery> {
constructor(private userService: UserService) {}
async execute(query: GetUserQuery): Promise<UserDto> {
return this.userService.getUser(query.userId);
}
}CQRS Best Practices
- Use commands for write operations
- Use queries for read operations
- Keep commands and queries simple
- Register handlers in module providers
- Use proper typing with generics
- Separate command/query definitions from handlers
Authentication and Authorization
Auth Decorator Usage
// Role-based authentication
@Auth([RoleType.ADMIN]) // Admin only
@Auth([RoleType.USER]) // User only
@Auth([RoleType.USER, RoleType.ADMIN]) // User or Admin
// Get current user
@AuthUser() user: UserEntity // Inject current user
// Public endpoints (no @Auth decorator)
@Post('login') // Public login endpointJWT Token Structure
// Token payload interface
export class TokenPayloadDto {
@NumberField()
expiresIn: number;
@StringField()
token: string;
}
// Token creation
const token = await this.authService.createAccessToken({
userId: userEntity.id,
role: userEntity.role,
});Exception Handling
Custom Exception Pattern
import { NotFoundException } from '@nestjs/common';
export class UserNotFoundException extends NotFoundException {
constructor(error?: string) {
super('error.userNotFound', error);
}
}Configuration and Environment
Environment Variables Pattern
// Use UPPERCASE for environment variables
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=nest_boilerplate
JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
// Access in code via ApiConfigService (do not access process.env directly)
// configService.authConfig.privateKey
// configService.postgresConfig.hostConfiguration Service Pattern
@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
get isDevelopment(): boolean {
return this.nodeEnv === 'development';
}
get postgresConfig(): TypeOrmModuleOptions {
return {
type: 'postgres',
host: this.configService.get('DB_HOST'),
port: this.configService.get('DB_PORT'),
username: this.configService.get('DB_USERNAME'),
password: this.configService.get('DB_PASSWORD'),
database: this.configService.get('DB_DATABASE'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
};
}
}Summary
This NestJS boilerplate follows modern TypeScript and NestJS best practices with:
- Strict typing throughout the codebase
- Modular architecture with clear separation of concerns
- CQRS pattern for scalable command/query operations
- Custom decorators for validation and documentation
- Comprehensive error handling with custom exceptions
- JWT-based authentication with role-based authorization
- Swagger documentation auto-generated from decorators
- ESM modules with explicit .ts extensions
- Comprehensive testing setup with Jest
Use this guide as the definitive reference for all code generation and development work on this project.