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
SharedModule for 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
DEFAULT scope β only use REQUEST or TRANSIENT when justified
- Use constructor injection exclusively β avoid property injection
- Register custom providers with
useClass, useValue, useFactory, or useExisting
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
HttpException for HTTP-specific errors
- Create domain-specific exception classes (e.g.,
OrderNotFoundException)
- Implement a global
ExceptionFilter for 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
ValidationPipe globally with transform: true and whitelist: true
- Decorate all DTO properties with
class-validator decorators
- Use
class-transformer for 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:
@Module({
imports: [DatabaseModule],
controllers: [ProductController],
providers: [ProductService, ProductRepository],
exports: [ProductService],
})
export class ProductModule {}
2. Create validated DTOs:
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:
grep -r "ProductModule" src/app.module.ts
npx jest --testPathPattern="product"
Constraints and Warnings
- Do not mix scopes without justification β
REQUEST-scoped providers cascade to all dependents
- Never access database directly from controllers β always go through service and repository layers
- Avoid
forwardRef() β restructure modules to eliminate circular dependencies
- Do not skip
ValidationPipe β always validate at the API boundary with DTOs
- Never hardcode secrets β use
@nestjs/config with environment variables
- Keep modules focused β one domain feature per module, avoid "god modules"
References
references/architecture.md β Deep-dive into NestJS architectural patterns
references/ β Individual enforcement rules with correct/incorrect examples
assets/templates/ β Starter templates for common NestJS components