
Nestjs Best Practices
- 1.6k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
nestjs-best-practices is an agent skill for apply nestjs modular architecture, di scoping, validation, and drizzle integration patterns.
About
The nestjs-best-practices skill is designed for apply NestJS modular architecture, DI scoping, validation, and Drizzle integration patterns. When to Use Designing/refactoring NestJS modules or dependency injection Creating exception filters, validating DTOs, or integrating Drizzle ORM Reviewing code for anti-patterns or onboarding to a NestJS codebase Instructions 1. Modular Architecture Follow strict module encapsulation. Invoke when the user builds NestJS controllers, modules, DTO validation, or Drizzle services.
- Designing/refactoring NestJS modules or dependency injection.
- Creating exception filters, validating DTOs, or integrating Drizzle ORM.
- Reviewing code for anti-patterns or onboarding to a NestJS codebase.
- Export only what other modules need — keep internal providers private.
- Use forwardRef() only as a last resort for circular dependencies; prefer restructuring.
Nestjs Best Practices by the numbers
- 1,570 all-time installs (skills.sh)
- +58 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #307 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
nestjs-best-practices capabilities & compatibility
- Capabilities
- designing/refactoring nestjs modules or dependen · creating exception filters, validating dtos, or · reviewing code for anti patterns or onboarding t · export only what other modules need — keep inter
What nestjs-best-practices says it does
Provides comprehensive NestJS best practices including modular architecture, dependency injection scoping, exception filters, DTO validation with class-validator, and Drizzle ORM i
Provides comprehensive NestJS best practices including modular architecture, dependency injection scoping, exception filters, DTO validation with class-validato
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nestjs-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I apply nestjs modular architecture, di scoping, validation, and drizzle integration patterns?
Apply NestJS modular architecture, DI scoping, validation, and Drizzle integration patterns.
Who is it for?
Backend developers structuring production NestJS services and modules.
Skip if: Skip for Express-only APIs without NestJS framework context.
When should I use this skill?
User builds NestJS controllers, modules, DTO validation, or Drizzle services.
What you get
Completed nestjs-best-practices workflow with documented commands, files, and expected deliverables.
- module layouts
- validated DTOs
- exception filter implementations
Files
NestJS Best Practices
Overview
Grounded in the Official NestJS Documentation, this skill enforces modular architecture, dependency injection scoping, exception filters, DTO validation with class-validator, and Drizzle ORM integration patterns.
When to Use
- Designing/refactoring NestJS modules or dependency injection
- Creating exception filters, validating DTOs, or integrating Drizzle ORM
- Reviewing code for anti-patterns or onboarding to a NestJS codebase
Instructions
1. Modular Architecture
Follow strict module encapsulation. Each domain feature should be its own @Module():
- Export only what other modules need — keep internal providers private
- Use
forwardRef()only as a last resort for circular dependencies; prefer restructuring - Group related controllers, services, and repositories within the same module
- Use a
SharedModulefor cross-cutting concerns (logging, configuration, caching)
See references/arch-module-boundaries.md for enforcement rules.
2. Dependency Injection
Choose the correct provider scope based on use case:
| Scope | Lifecycle | Use Case |
|---|---|---|
DEFAULT | Singleton (shared) | Stateless services, repositories |
REQUEST | Per-request instance | Request-scoped data (tenant, user context) |
TRANSIENT | New instance per injection | Stateful utilities, per-consumer caches |
- Default to
DEFAULTscope — only useREQUESTorTRANSIENTwhen justified - Use constructor injection exclusively — avoid property injection
- Register custom providers with
useClass,useValue,useFactory, oruseExisting
See references/di-provider-scoping.md for enforcement rules.
3. Request Lifecycle
Understand and respect the NestJS request processing pipeline:
Middleware → Guards → Interceptors (before) → Pipes → Route Handler → Interceptors (after) → Exception Filters- Middleware: Cross-cutting concerns (logging, CORS, body parsing)
- Guards: Authorization and authentication checks (return
true/false) - Interceptors: Transform response data, add caching, measure timing
- Pipes: Validate and transform input parameters
- Exception Filters: Catch and format error responses
4. Error Handling
Standardize error responses across the application:
- Extend
HttpExceptionfor HTTP-specific errors - Create domain-specific exception classes (e.g.,
OrderNotFoundException) - Implement a global
ExceptionFilterfor consistent error formatting - Use the Result pattern for expected business logic failures
- Never silently swallow exceptions
See references/error-exception-filters.md for enforcement rules.
5. Validation
Enforce input validation at the API boundary:
- Enable
ValidationPipeglobally withtransform: trueandwhitelist: true - Decorate all DTO properties with
class-validatordecorators - Use
class-transformerfor type coercion (@Type(),@Transform()) - Create separate DTOs for Create, Update, and Response operations
- Never trust raw user input — validate everything
See references/api-validation-dto.md for enforcement rules.
6. Database Patterns (Drizzle ORM)
Integrate Drizzle ORM following NestJS provider conventions:
- Wrap the Drizzle client in an injectable provider
- Use the Repository pattern for data access encapsulation
- Define schemas in dedicated schema files per domain module
- Use transactions for multi-step operations
- Keep database logic out of controllers
See references/db-drizzle-patterns.md for enforcement rules.
Best Practices
| Area | Do | Don't |
|---|---|---|
| Modules | One module per domain feature | Dump everything in AppModule |
| DI Scoping | Default to singleton scope | Use REQUEST scope without justification |
| Error Handling | Custom exception filters + domain errors | Bare try/catch with console.log |
| Validation | Global ValidationPipe + DTO decorators | Manual if checks in controllers |
| Database | Repository pattern with injected client | Direct DB queries in controllers |
| Testing | Unit test services, e2e test controllers | Skip tests or test implementation details |
| Configuration | @nestjs/config with typed schemas | Hardcode values or use process.env |
Examples
Example: New Domain Module with Validation
When building a "Product" feature, follow this workflow:
1. Create the module with proper encapsulation:
// product/product.module.ts
@Module({
imports: [DatabaseModule],
controllers: [ProductController],
providers: [ProductService, ProductRepository],
exports: [ProductService], // Only export what others need
})
export class ProductModule {}2. Create validated DTOs:
// product/dto/create-product.dto.ts
import { IsString, IsNumber, IsPositive, MaxLength } from 'class-validator';
export class CreateProductDto {
@IsString() @MaxLength(255) readonly name: string;
@IsNumber() @IsPositive() readonly price: number;
}3. Service with error handling:
@Injectable()
export class ProductService {
constructor(private readonly productRepository: ProductRepository) {}
async findById(id: string): Promise<Product> {
const product = await this.productRepository.findById(id);
if (!product) throw new ProductNotFoundException(id);
return product;
}
}4. Verify module registration:
# Check module is imported in AppModule
grep -r "ProductModule" src/app.module.ts
# Run e2e to confirm exports work
npx jest --testPathPattern="product"Constraints and Warnings
1. Do not mix scopes without justification — REQUEST-scoped providers cascade to all dependents 2. Never access database directly from controllers — always go through service and repository layers 3. Avoid `forwardRef()` — restructure modules to eliminate circular dependencies 4. Do not skip `ValidationPipe` — always validate at the API boundary with DTOs 5. Never hardcode secrets — use @nestjs/config with environment variables 6. Keep modules focused — one domain feature per module, avoid "god modules"
References
references/architecture.md— Deep-dive into NestJS architectural patternsreferences/— Individual enforcement rules with correct/incorrect examplesassets/templates/— Starter templates for common NestJS components
/**
* NestJS Controller Template
*
* Usage: Copy this template and replace `Feature` with your domain name.
* Implements standard REST endpoints with proper validation and response types.
*/
import {
Controller,
Get,
Post,
Patch,
Delete,
Param,
Body,
ParseUUIDPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { FeatureService } from './feature.service';
import { CreateFeatureDto } from './dto/create-feature.dto';
import { UpdateFeatureDto } from './dto/update-feature.dto';
import { FeatureResponseDto } from './dto/feature-response.dto';
@Controller('features')
export class FeatureController {
constructor(private readonly featureService: FeatureService) {}
@Get()
async findAll(): Promise<FeatureResponseDto[]> {
return this.featureService.findAll();
}
@Get(':id')
async findById(
@Param('id', ParseUUIDPipe) id: string,
): Promise<FeatureResponseDto> {
return this.featureService.findById(id);
}
@Post()
async create(@Body() dto: CreateFeatureDto): Promise<FeatureResponseDto> {
return this.featureService.create(dto);
}
@Patch(':id')
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateFeatureDto,
): Promise<FeatureResponseDto> {
return this.featureService.update(id, dto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async delete(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
return this.featureService.delete(id);
}
}
/**
* NestJS Domain Module Template
*
* Usage: Copy this template and replace `Feature` with your domain name.
* Example: OrderModule, UserModule, ProductModule
*/
import { Module } from '@nestjs/common';
import { FeatureController } from './feature.controller';
import { FeatureService } from './feature.service';
import { FeatureRepository } from './feature.repository';
import { DatabaseModule } from '../database/database.module';
@Module({
imports: [DatabaseModule],
controllers: [FeatureController],
providers: [FeatureService, FeatureRepository],
exports: [FeatureService],
})
export class FeatureModule {}
/**
* NestJS Service Template
*
* Usage: Copy this template and replace `Feature` with your domain name.
* Implements standard CRUD operations with proper error handling.
*/
import { Injectable, Logger } from '@nestjs/common';
import { FeatureRepository } from './feature.repository';
import { CreateFeatureDto } from './dto/create-feature.dto';
import { UpdateFeatureDto } from './dto/update-feature.dto';
import { FeatureNotFoundException } from './exceptions/feature-not-found.exception';
@Injectable()
export class FeatureService {
private readonly logger = new Logger(FeatureService.name);
constructor(private readonly featureRepository: FeatureRepository) {}
async findAll(): Promise<Feature[]> {
return this.featureRepository.findAll();
}
async findById(id: string): Promise<Feature> {
const feature = await this.featureRepository.findById(id);
if (feature === null) {
throw new FeatureNotFoundException(id);
}
return feature;
}
async create(dto: CreateFeatureDto): Promise<Feature> {
this.logger.log(`Creating feature with id: ${dto.constructor.name}`); // Avoid logging full DTOs — may contain PII
return this.featureRepository.save(dto);
}
async update(id: string, dto: UpdateFeatureDto): Promise<Feature> {
await this.findById(id); // Throws if not found
return this.featureRepository.update(id, dto);
}
async delete(id: string): Promise<void> {
await this.findById(id); // Throws if not found
await this.featureRepository.delete(id);
}
}
Rule: API Validation and DTOs
Context
Input validation is critical for security and data integrity. NestJS provides ValidationPipe with class-validator to enforce validation rules declaratively via DTO decorators.
Guidelines
ValidationPipe Configuration
- Enable
ValidationPipeglobally inmain.tswith these options: transform: true— auto-transform payloads to DTO instanceswhitelist: true— strip properties not defined in the DTOforbidNonWhitelisted: true— reject requests with unknown propertiesforbidUnknownValues: true— reject unknown objects
DTO Design
- Create separate DTOs for each operation:
CreateOrderDto,UpdateOrderDto,OrderResponseDto - Use
class-validatordecorators on every property (@IsString(),@IsNumber(),@IsEmail(), etc.) - Use
class-transformerdecorators for type coercion (@Type(),@Transform()) - Use
@IsOptional()for optional fields — combine with a validation decorator - Use
PartialType(),PickType(),OmitType()from@nestjs/mapped-typesto derive DTOs
Nested Validation
- Use
@ValidateNested()with@Type(() => NestedDto)for nested objects - Use
@IsArray()with@ValidateNested({ each: true })for arrays of objects
Validation Location
- Validate at the API boundary (controllers) — never deep inside business logic
- Let
ValidationPipehandle formatting error responses automatically - Use
@UsePipes()for endpoint-specific pipe configurations
Examples
✅ Correct — Global ValidationPipe Setup
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}),
);
await app.listen(3000);
}✅ Correct — DTO with Validation Decorators
// order/dto/create-order.dto.ts
import { IsString, IsNumber, IsPositive, ValidateNested, IsArray } from 'class-validator';
import { Type } from 'class-transformer';
export class OrderItemDto {
@IsString()
readonly productId: string;
@IsNumber()
@IsPositive()
readonly quantity: number;
}
export class CreateOrderDto {
@IsString()
readonly customerId: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
readonly items: OrderItemDto[];
}✅ Correct — Derived DTOs with Mapped Types
import { PartialType, OmitType } from '@nestjs/mapped-types';
// UpdateOrderDto has all CreateOrderDto fields as optional
export class UpdateOrderDto extends PartialType(CreateOrderDto) {}
// CreateOrderDto without customerId (set from auth context)
export class InternalCreateOrderDto extends OmitType(CreateOrderDto, ['customerId']) {}✅ Correct — Controller Using DTOs
@Controller('orders')
export class OrderController {
constructor(private readonly orderService: OrderService) {}
@Post()
async create(@Body() dto: CreateOrderDto): Promise<OrderResponseDto> {
return this.orderService.create(dto);
}
@Patch(':id')
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateOrderDto,
): Promise<OrderResponseDto> {
return this.orderService.update(id, dto);
}
}❌ Incorrect — No Validation on DTO
// Missing class-validator decorators — no validation occurs
export class CreateOrderDto {
customerId: string; // No @IsString()
quantity: number; // No @IsNumber(), no @IsPositive()
}❌ Incorrect — Manual Validation in Controller
@Post()
async create(@Body() body: any) {
// Wrong: manual validation instead of using ValidationPipe + DTOs
if (!body.customerId || typeof body.customerId !== 'string') {
throw new BadRequestException('customerId is required');
}
if (!body.quantity || body.quantity < 0) {
throw new BadRequestException('quantity must be positive');
}
return this.orderService.create(body);
}Rule: Module Boundaries and Encapsulation
Context
NestJS modules define the boundaries of your application's domain features. Proper encapsulation prevents tight coupling and makes the codebase scalable and testable.
Guidelines
Module Organization
- Each domain feature must have its own
@Module()(e.g.,OrderModule,UserModule) - Only export providers that other modules genuinely need — keep everything else private
- Import only the modules you depend on — avoid importing unrelated modules
- Use a
SharedModulefor cross-cutting utilities (logging, caching, configuration)
Circular Dependencies
- Avoid `forwardRef()` by default — it is a code smell indicating poor module design
- If two modules depend on each other, extract shared logic into a third module
- Only use
forwardRef()when architectural constraints make extraction impractical - Document every
forwardRef()usage with a comment explaining why it's necessary
Module Registration
- Use
forRoot()/forRootAsync()for global singleton modules (database, config) - Use
forFeature()for domain-specific module registration - Use dynamic modules for configurable providers
Examples
✅ Correct — Proper Module Encapsulation
// order/order.module.ts
@Module({
imports: [PaymentModule, UserModule],
controllers: [OrderController],
providers: [OrderService, OrderRepository],
exports: [OrderService], // Only export what others need
})
export class OrderModule {}
// payment/payment.module.ts
@Module({
controllers: [PaymentController],
providers: [PaymentService, PaymentGateway],
exports: [PaymentService], // Exported for OrderModule to use
})
export class PaymentModule {}❌ Incorrect — God Module Anti-Pattern
// app.module.ts — everything dumped in root module
@Module({
controllers: [
OrderController,
UserController,
PaymentController,
ProductController,
],
providers: [
OrderService,
UserService,
PaymentService,
ProductService,
OrderRepository,
UserRepository,
],
})
export class AppModule {} // No encapsulation, all providers are shared❌ Incorrect — Unnecessary forwardRef
// Instead of this circular dependency:
@Module({
imports: [forwardRef(() => UserModule)],
providers: [OrderService],
})
export class OrderModule {}
@Module({
imports: [forwardRef(() => OrderModule)],
providers: [UserService],
})
export class UserModule {}
// ✅ Extract shared logic into a third module:
@Module({
providers: [UserOrderLinkService],
exports: [UserOrderLinkService],
})
export class UserOrderModule {}NestJS Architectural Patterns Reference
Module Architecture
Domain Module Pattern
Every bounded context in the application should map to a NestJS module. A well-structured module contains:
order/
├── order.module.ts # Module definition
├── order.controller.ts # HTTP layer
├── order.service.ts # Business logic
├── order.repository.ts # Data access
├── dto/ # Data Transfer Objects
│ ├── create-order.dto.ts
│ ├── update-order.dto.ts
│ └── order-response.dto.ts
├── entities/ # Domain entities
│ └── order.entity.ts
├── exceptions/ # Domain-specific exceptions
│ └── order-not-found.exception.ts
├── schemas/ # Drizzle ORM schemas
│ └── order.schema.ts
├── guards/ # Module-specific guards
├── interceptors/ # Module-specific interceptors
└── __tests__/ # Co-located tests
├── order.controller.spec.ts
└── order.service.spec.tsShared Module Pattern
Cross-cutting concerns belong in a SharedModule:
@Module({
providers: [
LoggerService,
CacheService,
PaginationHelper,
],
exports: [
LoggerService,
CacheService,
PaginationHelper,
],
})
export class SharedModule {}Import SharedModule in any module that needs these utilities.
Configuration Module
Use @nestjs/config with typed configuration:
// config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
name: process.env.DB_NAME || 'myapp',
url: process.env.DATABASE_URL,
}));
// Usage in a provider
@Injectable()
export class DatabaseService {
constructor(
@Inject(databaseConfig.KEY)
private readonly dbConfig: ConfigType<typeof databaseConfig>,
) {}
}Request Lifecycle Deep-Dive
Execution Order
1. Incoming Request
2. Globally-bound middleware
3. Module-bound middleware
4. Global guards
5. Controller guards
6. Route guards
7. Global interceptors (pre-controller)
8. Controller interceptors (pre-controller)
9. Route interceptors (pre-controller)
10. Global pipes
11. Controller pipes
12. Route pipes
13. Route parameter pipes
14. Controller method (route handler)
15. Route interceptors (post-request)
16. Controller interceptors (post-request)
17. Global interceptors (post-request)
18. Exception filters (route → controller → global)
19. ResponseGuard Pattern — Authentication
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Missing authentication token');
}
try {
const payload = await this.jwtService.verifyAsync(token);
request['user'] = payload;
return true;
} catch {
throw new UnauthorizedException('Invalid authentication token');
}
}
private extractToken(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}Interceptor Pattern — Response Transformation
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}Testing Strategy
Unit Tests
Test services in isolation with mocked dependencies:
describe('OrderService', () => {
let service: OrderService;
let repository: jest.Mocked<OrderRepository>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
OrderService,
{
provide: OrderRepository,
useValue: {
findById: jest.fn(),
save: jest.fn(),
},
},
],
}).compile();
service = module.get(OrderService);
repository = module.get(OrderRepository);
});
it('should throw OrderNotFoundException when order not found', async () => {
repository.findById.mockResolvedValue(null);
await expect(service.findById('non-existent'))
.rejects
.toThrow(OrderNotFoundException);
});
});E2E Tests
Test the full HTTP lifecycle:
describe('OrderController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
await app.init();
});
it('POST /orders — should validate input', () => {
return request(app.getHttpServer())
.post('/orders')
.send({ invalidField: true })
.expect(400);
});
afterAll(async () => {
await app.close();
});
});Rule: Drizzle ORM Integration Patterns
Context
Drizzle ORM is a lightweight, type-safe ORM for TypeScript. When used within NestJS, it must be integrated through the provider system to maintain testability and proper dependency management.
Guidelines
Drizzle Client Provider
- Wrap the Drizzle client in a custom provider using
useFactory - Register the provider in a dedicated
DatabaseModule - Export the provider token so domain modules can inject it
- Use
ConfigServicefor database connection configuration — never hardcode
Repository Pattern
- Create a repository class per domain entity (e.g.,
OrderRepository) - Inject the Drizzle client via constructor injection using the provider token
- Encapsulate all database queries inside repositories — never expose Drizzle directly to services
- Return domain entities from repository methods, not raw query results
Schema Organization
- Define Drizzle schemas in dedicated files per domain (e.g.,
order/schemas/order.schema.ts) - Co-locate schemas with their domain module
- Use Drizzle's type inference for TypeScript types:
typeof orders.$inferSelect
Transactions
- Use
db.transaction()for operations that span multiple tables - Pass the transaction client as a parameter to repository methods
- Handle transaction rollback via exception propagation
Examples
✅ Correct — Database Module with Factory Provider
// database/database.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
export const DRIZZLE = Symbol('DRIZZLE');
@Module({
providers: [
{
provide: DRIZZLE,
useFactory: async (configService: ConfigService) => {
const pool = new Pool({
connectionString: configService.get<string>('DATABASE_URL'),
});
return drizzle(pool);
},
inject: [ConfigService],
},
],
exports: [DRIZZLE],
})
export class DatabaseModule {}✅ Correct — Repository with Injected Drizzle Client
// order/order.repository.ts
import { Inject, Injectable } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { eq } from 'drizzle-orm';
import { DRIZZLE } from '../database/database.module';
import { orders } from './schemas/order.schema';
@Injectable()
export class OrderRepository {
constructor(
@Inject(DRIZZLE) private readonly db: NodePgDatabase,
) {}
async findById(id: string): Promise<Order | null> {
const result = await this.db
.select()
.from(orders)
.where(eq(orders.id, id))
.limit(1);
return result[0] ?? null;
}
async save(data: NewOrder): Promise<Order> {
const result = await this.db
.insert(orders)
.values(data)
.returning();
return result[0];
}
}✅ Correct — Schema Definition
// order/schemas/order.schema.ts
import { pgTable, uuid, varchar, integer, timestamp } from 'drizzle-orm/pg-core';
export const orders = pgTable('orders', {
id: uuid('id').primaryKey().defaultRandom(),
customerId: uuid('customer_id').notNull(),
status: varchar('status', { length: 50 }).notNull().default('pending'),
totalAmount: integer('total_amount').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export type Order = typeof orders.$inferSelect;
export type NewOrder = typeof orders.$inferInsert;✅ Correct — Transaction Usage
@Injectable()
export class OrderService {
constructor(
@Inject(DRIZZLE) private readonly db: NodePgDatabase,
private readonly orderRepository: OrderRepository,
) {}
async createOrderWithItems(dto: CreateOrderDto): Promise<Order> {
return this.db.transaction(async (tx) => {
const order = await tx
.insert(orders)
.values({ customerId: dto.customerId, totalAmount: dto.totalAmount })
.returning();
await tx.insert(orderItems).values(
dto.items.map((item) => ({
orderId: order[0].id,
productId: item.productId,
quantity: item.quantity,
})),
);
return order[0];
});
}
}❌ Incorrect — Direct DB Access in Controller
@Controller('orders')
export class OrderController {
constructor(@Inject(DRIZZLE) private readonly db: NodePgDatabase) {}
@Get(':id')
async findById(@Param('id') id: string) {
// Wrong: database access should be in a repository, not controller
return this.db.select().from(orders).where(eq(orders.id, id));
}
}❌ Incorrect — Hardcoded Connection String
// Wrong: hardcoded database URL
const pool = new Pool({
connectionString: 'postgresql://user:pass@localhost:5432/mydb',
});
export const db = drizzle(pool);
// ✅ Correct: use ConfigService via a provider factoryRule: Dependency Injection and Provider Scoping
Context
NestJS provides three injection scopes (DEFAULT, REQUEST, TRANSIENT) that control provider lifecycle and instance sharing. Choosing the wrong scope leads to memory leaks, shared state bugs, or unnecessary overhead.
Guidelines
Scope Selection
- `DEFAULT` (Singleton): Use for stateless services, repositories, and utilities — the vast majority of providers
- `REQUEST`: Use only when the provider needs request-specific data (e.g., tenant context, authenticated user)
- `TRANSIENT`: Use when each consumer needs its own isolated instance (e.g., per-consumer loggers, stateful builders)
Scope Propagation
- A
REQUEST-scoped provider forces all its dependents to also become request-scoped - Minimize
REQUESTscope usage to avoid cascading performance impact - Use
@Inject(REQUEST)to access the request object in request-scoped providers
Injection Patterns
- Always use constructor injection — never property injection with
@Inject()on class fields - Use
@Injectable()on every provider class - Use
@Inject('TOKEN')for custom provider tokens - Use
@Optional()for providers that may not be available
Custom Providers
- Use
useClassto swap implementations (e.g., testing mocks) - Use
useValuefor constants and configuration objects - Use
useFactoryfor providers requiring async initialization or complex setup - Use
useExistingto alias an existing provider under a new token
Examples
✅ Correct — Singleton (DEFAULT) Scope
@Injectable() // DEFAULT scope — singleton, shared across the app
export class OrderService {
constructor(
private readonly orderRepository: OrderRepository,
private readonly paymentService: PaymentService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
return this.orderRepository.save(dto);
}
}✅ Correct — REQUEST Scope with Justification
// Only use REQUEST scope when you need per-request state
@Injectable({ scope: Scope.REQUEST })
export class TenantService {
constructor(@Inject(REQUEST) private readonly request: Request) {}
getTenantId(): string {
return this.request.headers['x-tenant-id'] as string;
}
}✅ Correct — Custom Factory Provider
// Async factory for providers needing initialization
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const dbUrl = configService.get<string>('DATABASE_URL');
return drizzle(dbUrl);
},
inject: [ConfigService],
},
],
exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}❌ Incorrect — Unnecessary REQUEST Scope
// This service has no request-specific state — should be DEFAULT
@Injectable({ scope: Scope.REQUEST }) // Wrong: causes performance overhead
export class MathService {
add(a: number, b: number): number {
return a + b;
}
}❌ Incorrect — Property Injection
@Injectable()
export class OrderService {
@Inject() // Wrong: use constructor injection instead
private orderRepository: OrderRepository;
}✅ Correct — Constructor Injection
@Injectable()
export class OrderService {
constructor(
private readonly orderRepository: OrderRepository,
) {}
}Rule: Error Handling and Exception Filters
Context
NestJS provides a built-in exception layer that processes unhandled exceptions. Standardizing error handling ensures consistent API responses and proper error classification.
Guidelines
Exception Hierarchy
- Create a base
AppExceptionextendingHttpExceptionfor all domain errors - Create specific exception classes per domain (e.g.,
OrderNotFoundException,InsufficientStockException) - Include meaningful error messages and relevant context
- Always set the appropriate HTTP status code
Exception Filters
- Implement a global
ExceptionFilterto catch allAppExceptioninstances - Return a consistent error response format across all endpoints
- Log exceptions with structured context (request ID, user, endpoint)
- Handle unknown exceptions with a generic 500 response — never expose internal details
Error Response Format
Use a consistent structure for all error responses:
{
"type": "OrderNotFoundException",
"title": "Order with ID '12345' was not found",
"status": 404,
"timestamp": "2025-01-15T10:30:00Z",
"path": "/api/orders/12345"
}Best Practices
- Throw exceptions from services — let the exception filter handle formatting
- Use
try/catchonly at error boundaries (controllers, middleware), not in services - Preserve the error chain with
{ cause: originalError }option - Never silently swallow exceptions (empty catch blocks)
- Use the Result pattern for expected business logic failures that aren't exceptional
Examples
✅ Correct — Domain Exception Hierarchy
// common/exceptions/app.exception.ts
export abstract class AppException extends HttpException {
constructor(
message: string,
status: HttpStatus,
options?: HttpExceptionOptions,
) {
super(message, status, options);
}
}
// order/exceptions/order-not-found.exception.ts
export class OrderNotFoundException extends AppException {
constructor(orderId: string) {
super(
`Order with ID '${orderId}' was not found`,
HttpStatus.NOT_FOUND,
);
}
}
// order/exceptions/insufficient-stock.exception.ts
export class InsufficientStockException extends AppException {
constructor(productId: string) {
super(
`Insufficient stock for product '${productId}'`,
HttpStatus.CONFLICT,
);
}
}✅ Correct — Global Exception Filter
@Catch(AppException)
export class AppExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AppExceptionFilter.name);
catch(exception: AppException, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
this.logger.error(exception.message, exception.stack);
response.status(status).json({
type: exception.constructor.name,
title: exception.message,
status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}✅ Correct — Service Throwing Domain Exceptions
@Injectable()
export class OrderService {
constructor(private readonly orderRepository: OrderRepository) {}
async findById(orderId: string): Promise<Order> {
const order = await this.orderRepository.findById(orderId);
if (order === null) {
throw new OrderNotFoundException(orderId);
}
return order;
}
}❌ Incorrect — Raw HttpException in Service
@Injectable()
export class OrderService {
async findById(orderId: string): Promise<Order> {
const order = await this.orderRepository.findById(orderId);
if (!order) {
// Wrong: use domain-specific exception, not raw HttpException
throw new HttpException('Not found', 404);
}
return order;
}
}❌ Incorrect — Error Handling in Controller
@Controller('orders')
export class OrderController {
@Get(':id')
async findById(@Param('id') id: string) {
try {
return await this.orderService.findById(id);
} catch (error) {
// Wrong: don't handle errors in controllers — let exception filters do it
console.log(error);
return { error: 'Something went wrong' };
}
}
}
// ✅ Correct: let the exception propagate to the filter
@Controller('orders')
export class OrderController {
@Get(':id')
async findById(@Param('id') id: string): Promise<Order> {
return this.orderService.findById(id);
}
}Related skills
Forks & variants (1)
Nestjs Best Practices has 1 known copy in the catalog totaling 4 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 4 installs
How it compares
Pick nestjs-best-practices for opinionated NestJS architecture enforcement; use generic TypeScript skills when you are not on the NestJS module and DI model.
FAQ
What does nestjs-best-practices do?
Apply NestJS modular architecture, DI scoping, validation, and Drizzle integration patterns.
When should I use nestjs-best-practices?
User builds NestJS controllers, modules, DTO validation, or Drizzle services.
Is nestjs-best-practices safe to install?
Review the Security Audits panel on this page before installing in production.