NestJS Best Practices Comprehensive guide for building production-ready NestJS applications. Contains 26 rules across 13 categories, covering security, architecture, performance, validation, database operations, authentication, and advanced patterns. When to Apply Reference these guidelines when: - Writing new NestJS modules, controllers, or services - Implementing authentication and authorization - Setting up Prisma database operations - Creating DTOs and validation pipes - Adding middleware or guards - Implementing error handling and logging - Reviewing NestJS code for best practices - Refa…

)) {\n const isValid = this.verifyOldHash(password, hash);\n\n if (isValid) {\n // Rehash with Bun's secure algorithm\n const newHash = await this.hash(password);\n return { isValid: true, needsMigration: true, newHash };\n }\n\n return { isValid: false, needsMigration: false };\n }\n\n // Use Bun's native verify\n const isValid = await Bun.password.verify(password, hash);\n return { isValid, needsMigration: false };\n }\n\n private verifyOldHash(password: string, hash: string): boolean {\n // Old verification logic\n return true;\n }\n}\n```\n\n## Installation\n\n```bash\n# No packages needed! Bun's Crypto is built-in.\n# Just ensure you're using Bun runtime\nbun --version\n```\n\n## Quick Reference Checklist\n\nUse this checklist when reviewing or creating password handling:\n\n- [ ] Passwords are never stored in plain text\n- [ ] Passwords are hashed with `Bun.password.hash()`\n- [ ] Passwords are verified with `Bun.password.verify()`\n- [ ] Passwords have minimum length requirement (12+ characters)\n- [ ] Passwords require mixed case, numbers, special characters\n- [ ] Database column for hashed password is `TEXT` or `VARCHAR(255)`\n- [ ] Error messages don't reveal if user exists\n\n**Incorrect:**\n\n```typescript\n// auth/auth.service.ts - Insecure 🚨\nimport { Injectable } from '@nestjs/common';\nimport { createHash, randomBytes } from 'crypto';\n\n@Injectable()\nexport class AuthService {\n // ❌ Plain text storage\n async register(email: string, password: string) {\n return this.prisma.user.create({\n data: { email, password }, // ❌ Stored as-is!\n });\n }\n\n // ❌ Fast hash (SHA256, MD5) - crackable with GPUs\n async hashPassword(password: string) {\n return createHash('sha256').update(password).digest('hex');\n }\n\n // ❌ Manual salt - still vulnerable to GPU cracking\n async hashWithSalt(password: string) {\n const salt = randomBytes(16).toString('hex');\n const hash = createHash('sha512')\n .update(password + salt)\n .digest('hex');\n return `${salt}:${hash}`;\n }\n\n // ❌ Timing-sensitive string comparison\n async verifyPassword(password: string, hash: string) {\n const [salt, originalHash] = hash.split(':');\n const computedHash = createHash('sha512')\n .update(password + salt)\n .digest('hex');\n return computedHash === originalHash; // ❌ Timing attack vulnerable\n }\n\n // ❌ Weak password requirements\n @MinLength(6) // ❌ Too short!\n password: string;\n}\n```\n\n**Correct:**\n\n```typescript\n// auth/password.service.ts - Secure ✅\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class PasswordService {\n /**\n * Hash a password using Bun's built-in argon2 implementation.\n * Automatically handles salt generation and hashing parameters.\n */\n async hash(password: string): Promise\u003cstring> {\n return Bun.password.hash(password);\n }\n\n /**\n * Verify a password against a stored hash.\n * Uses timing-safe comparison to prevent timing attacks.\n */\n async verify(password: string, hash: string): Promise\u003cboolean> {\n return Bun.password.verify(password, hash);\n }\n\n /**\n * Hash with custom algorithm and options\n * Bun supports both argon2id (default, more secure) and bcrypt (widely compatible)\n * Algorithm is stored in the hash prefix, so verify() auto-detects it\n */\n async hashWithOptions(password: string): Promise\u003cstring> {\n return Bun.password.hash(password, {\n algorithm: 'argon2id', // 'argon2id' or 'bcrypt'\n memoryCost: 16, // Memory in MB (argon2 only)\n timeCost: 3, // Number of iterations\n });\n }\n\n /**\n * Hash with bcrypt for legacy compatibility\n * Use when migrating from bcrypt or need broader library compatibility\n */\n async hashWithBcrypt(password: string): Promise\u003cstring> {\n return Bun.password.hash(password, {\n algorithm: 'bcrypt',\n cost: 12, // bcrypt work factor (4-31, default 10)\n });\n }\n\n /**\n * Generate a secure random password for reset tokens\n */\n generateSecureToken(length: number = 32): string {\n const bytes = new Uint8Array(length);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');\n }\n}\n\n// auth/auth.service.ts - Secure ✅\nimport { Injectable, UnauthorizedException } from '@nestjs/common';\nimport { JwtService } from '@nestjs/jwt';\nimport { PasswordService } from './password.service';\n\n@Injectable()\nexport class AuthService {\n constructor(\n private prisma: PrismaService,\n private passwordService: PasswordService,\n private jwtService: JwtService,\n ) {}\n\n /**\n * Register a new user with secure password hashing\n */\n async register(dto: RegisterDto) {\n // ✅ Check if user exists first\n const existingUser = await this.prisma.user.findUnique({\n where: { email: dto.email },\n });\n\n if (existingUser) {\n // ✅ Generic error message - don't reveal if user exists\n throw new BadRequestException('Unable to create account');\n }\n\n // ✅ Hash password with Bun's secure implementation\n const hashedPassword = await this.passwordService.hash(dto.password);\n\n const user = await this.prisma.user.create({\n data: {\n email: dto.email,\n password: hashedPassword,\n // ✅ Store only the hash\n },\n select: {\n id: true,\n email: true,\n createdAt: true,\n // ✅ Never return password in responses\n },\n });\n\n // ✅ Generate tokens\n const tokens = await this.generateTokens(user);\n\n return {\n user,\n ...tokens,\n };\n }\n\n /**\n * Login with timing-safe password verification\n */\n async login(dto: LoginDto) {\n // ✅ Always perform lookup to prevent timing attacks\n const user = await this.prisma.user.findUnique({\n where: { email: dto.email },\n });\n\n if (!user) {\n // ✅ Use the same error message as invalid password\n throw new UnauthorizedException('Invalid credentials');\n }\n\n // ✅ Timing-safe password verification\n const isValid = await this.passwordService.verify(dto.password, user.password);\n\n if (!isValid) {\n // ✅ Same error message - don't reveal if user exists\n throw new UnauthorizedException('Invalid credentials');\n }\n\n // ✅ Check if hash needs rehashing (algorithm upgraded)\n if (Bun.password.needsRehash(user.password)) {\n // Rehash with current best algorithm\n const newHash = await this.passwordService.hash(dto.password);\n await this.prisma.user.update({\n where: { id: user.id },\n data: { password: newHash },\n });\n }\n\n return this.generateTokens(user);\n }\n\n /**\n * Change password with old password verification\n */\n async changePassword(userId: string, dto: ChangePasswordDto) {\n const user = await this.prisma.user.findUnique({\n where: { id: userId },\n });\n\n if (!user) {\n throw new UnauthorizedException('Invalid credentials');\n }\n\n // ✅ Verify old password\n const isValid = await this.passwordService.verify(dto.oldPassword, user.password);\n\n if (!isValid) {\n throw new UnauthorizedException('Invalid credentials');\n }\n\n // ✅ Hash new password\n const newHashedPassword = await this.passwordService.hash(dto.newPassword);\n\n await this.prisma.user.update({\n where: { id: userId },\n data: { password: newHashedPassword },\n });\n\n return { message: 'Password changed successfully' };\n }\n\n /**\n * Reset password with secure token\n */\n async resetPassword(token: string, newPassword: string) {\n // Find valid reset token\n const resetToken = await this.prisma.passwordResetToken.findUnique({\n where: { token },\n include: { user: true },\n });\n\n if (!resetToken || resetToken.expiresAt \u003c new Date()) {\n throw new BadRequestException('Invalid or expired reset token');\n }\n\n // ✅ Hash new password\n const hashedPassword = await this.passwordService.hash(newPassword);\n\n await this.prisma.$transaction([\n this.prisma.user.update({\n where: { id: resetToken.user.id },\n data: { password: hashedPassword },\n }),\n this.prisma.passwordResetToken.delete({\n where: { id: resetToken.id },\n }),\n ]);\n\n return { message: 'Password reset successfully' };\n }\n\n private async generateTokens(user: any) {\n const payload = { sub: user.id, email: user.email };\n const accessToken = await this.jwtService.signAsync(payload);\n return { accessToken };\n }\n}\n\n// auth/dto/register.dto.ts - Strong password requirements ✅\nimport { IsEmail, IsString, MinLength, Matches } from 'class-validator';\n\nexport class RegisterDto {\n @IsEmail({}, { message: 'Please provide a valid email address' })\n email: string;\n\n @IsString()\n @MinLength(12, { message: 'Password must be at least 12 characters long' })\n @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])/, {\n message: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character (@$!%*?&)',\n })\n password: string;\n}\n\n// auth/dto/change-password.dto.ts ✅\nimport { IsString, MinLength, Matches } from 'class-validator';\n\nexport class ChangePasswordDto {\n @IsString()\n oldPassword: string;\n\n @IsString()\n @MinLength(12)\n @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])/)\n newPassword: string;\n}\n\n// prisma/schema.prisma - Correct schema ✅\nmodel User {\n id String @id @default(uuid())\n email String @unique\n password String // ✅ Store hash as TEXT (can be up to 255 chars)\n\n // ✅ Index for faster login lookups\n @@index([email])\n}\n```\n\n## Algorithm Selection: Argon2 vs Bcrypt\n\nBun's password hashing supports both algorithms at runtime. The algorithm choice is stored in the hash prefix, so `verify()` auto-detects which algorithm was used.\n\n### When to Use Argon2 (Default)\n\n```typescript\n// ✅ Recommended for new applications\nasync hash(password: string) {\n return Bun.password.hash(password); // Uses argon2id by default\n}\n\n// Or explicitly\nasync hash(password: string) {\n return Bun.password.hash(password, {\n algorithm: 'argon2id',\n memoryCost: 16, // MB of memory (higher = more GPU-resistant)\n timeCost: 2, // Number of iterations\n });\n}\n```\n\n**Use argon2id when:**\n- Starting a new application (recommended default)\n- Maximum security against GPU/ASIC attacks is required\n- Memory-hard hashing is acceptable\n- Compliance requirements mandate modern algorithms\n\n### When to Use Bcrypt\n\n```typescript\n// ✅ For legacy compatibility or cross-platform needs\nasync hash(password: string) {\n return Bun.password.hash(password, {\n algorithm: 'bcrypt',\n cost: 12, // Work factor (2^12 iterations)\n });\n}\n```\n\n**Use bcrypt when:**\n- Migrating from existing bcrypt hashes\n- Need compatibility with older systems/libraries\n- Memory constraints prevent argon2 usage\n- Regulatory requirements specifically mandate bcrypt\n\n### Runtime Algorithm Flexibility\n\n```typescript\n// ✅ Configure algorithm per environment\n@Injectable()\nexport class PasswordService {\n constructor(private configService: ConfigService) {}\n\n private get algorithm(): 'argon2id' | 'bcrypt' {\n // Runtime configuration from environment\n return this.configService.get('PASSWORD_ALGORITHM', 'argon2id');\n }\n\n async hash(password: string): Promise\u003cstring> {\n return Bun.password.hash(password, { algorithm: this.algorithm });\n }\n\n // ✅ verify() auto-detects algorithm from hash prefix\n async verify(password: string, hash: string): Promise\u003cboolean> {\n return Bun.password.verify(password, hash);\n }\n}\n```\n\n### Hash Format and Auto-Detection\n\nBun stores the algorithm in the hash prefix, enabling seamless migration:\n\n```typescript\n// Argon2 hash format: $argon2id$v=19$m=16,t=3,p=1$...\nconst argon2Hash = '$argon2id$v=19$m=16,t=3,p=1$salt$hash';\n\n// Bcrypt hash format: $2b$12$...\nconst bcryptHash = '$2b$12$salt$hash';\n\n// ✅ verify() auto-detects from prefix\nawait Bun.password.verify(password, argon2Hash); // Uses argon2\nawait Bun.password.verify(password, bcryptHash); // Uses bcrypt\n```\n\n### Migration Strategy\n\n```typescript\n// ✅ Gradual migration from bcrypt to argon2\n@Injectable()\nexport class PasswordService {\n async hash(password: string): Promise\u003cstring> {\n // Use argon2 for new passwords\n return Bun.password.hash(password, { algorithm: 'argon2id' });\n }\n\n async verify(password: string, hash: string): Promise\u003cboolean> {\n const isValid = await Bun.password.verify(password, hash);\n\n // Rehash bcrypt passwords with argon2 on successful login\n if (isValid && hash.startsWith('$2b

NestJS Best Practices Comprehensive guide for building production-ready NestJS applications. Contains 26 rules across 13 categories, covering security, architecture, performance, validation, database operations, authentication, and advanced patterns. When to Apply Reference these guidelines when: - Writing new NestJS modules, controllers, or services - Implementing authentication and authorization - Setting up Prisma database operations - Creating DTOs and validation pipes - Adding middleware or guards - Implementing error handling and logging - Reviewing NestJS code for best practices - Refa…

)) {\n // bcrypt detected - rehash with argon2\n const newHash = await this.hash(password);\n await this.updateUserPassword(newHash);\n }\n\n return isValid;\n }\n}\n```\n\n## Migration Strategy\n\n### Migrating from bcrypt/argon2 Packages\n\n```typescript\n// auth/migration/password-migration.service.ts ✅\nimport { Injectable, Logger } from '@nestjs/common';\nimport { PasswordService } from '../password.service';\n\n@Injectable()\nexport class PasswordMigrationService {\n private readonly logger = new Logger(PasswordMigrationService.name);\n\n constructor(\n private prisma: PrismaService,\n private passwordService: PasswordService,\n ) {}\n\n /**\n * Migrate all passwords to Bun's native hashing\n */\n async migratePasswords() {\n const users = await this.prisma.user.findMany({\n where: {\n // Identify old hashes by prefix or pattern\n password: { startsWith: '$2a

NestJS Best Practices Comprehensive guide for building production-ready NestJS applications. Contains 26 rules across 13 categories, covering security, architecture, performance, validation, database operations, authentication, and advanced patterns. When to Apply Reference these guidelines when: - Writing new NestJS modules, controllers, or services - Implementing authentication and authorization - Setting up Prisma database operations - Creating DTOs and validation pipes - Adding middleware or guards - Implementing error handling and logging - Reviewing NestJS code for best practices - Refa…

}, // bcrypt prefix\n },\n take: 100, // Process in batches\n });\n\n let migrated = 0;\n\n for (const user of users) {\n try {\n // Note: We can't directly migrate without knowing original passwords\n // Instead, rehash on next login\n\n this.logger.log(`User ${user.email} will be migrated on next login`);\n\n // Set a flag to indicate migration needed\n await this.prisma.user.update({\n where: { id: user.id },\n data: { needsPasswordMigration: true },\n });\n\n migrated++;\n } catch (error) {\n this.logger.error(`Failed to migrate user ${user.id}`, error);\n }\n }\n\n return { migrated, total: users.length };\n }\n\n /**\n * Migrate on login (in AuthService)\n */\n async migrateOnLogin(user: User, plainPassword: string): Promise\u003cvoid> {\n if (!user.needsPasswordMigration) {\n return;\n }\n\n // Verify with old method first\n const oldHashValid = await this.verifyOldPassword(plainPassword, user.password);\n\n if (oldHashValid) {\n // Rehash with Bun\n const newHash = await this.passwordService.hash(plainPassword);\n\n await this.prisma.user.update({\n where: { id: user.id },\n data: {\n password: newHash,\n needsPasswordMigration: false,\n },\n });\n\n this.logger.log(`Migrated password for user ${user.email}`);\n }\n }\n\n private async verifyOldPassword(password: string, hash: string): Promise\u003cboolean> {\n // Old verification logic (bcrypt, etc.)\n return true;\n }\n}\n```\n\n## Security Best Practices\n\n### Password Requirements\n\n```typescript\n// auth/constants/password-requirements.ts ✅\nexport const PASSWORD_REQUIREMENTS = {\n minLength: 12,\n maxLength: 128,\n requireUppercase: true,\n requireLowercase: true,\n requireNumbers: true,\n requireSpecialChars: true,\n allowedSpecialChars: '@$!%*?&',\n commonPasswordsBlacklist: [\n 'password', '123456', 'qwerty', 'admin', 'welcome',\n // Add more common passwords\n ],\n};\n```\n\n### Rate Limiting for Auth Endpoints\n\n```typescript\n// auth/auth.controller.ts ✅\nimport { Throttle } from '@nestjs/throttler';\n\n@Controller('auth')\nexport class AuthController {\n @Post('register')\n @Throttle({ default: { limit: 3, ttl: 3600000 } }) // 3 per hour\n async register(@Body() dto: RegisterDto) {\n return this.authService.register(dto);\n }\n\n @Post('login')\n @Throttle({ default: { limit: 5, ttl: 60000 } }) // 5 per minute\n async login(@Body() dto: LoginDto) {\n return this.authService.login(dto);\n }\n}\n```\n\n### Logging Security Events\n\n```typescript\n// auth/auth.service.ts ✅\n@Injectable()\nexport class AuthService {\n async login(dto: LoginDto) {\n const user = await this.prisma.user.findUnique({\n where: { email: dto.email },\n });\n\n const isValid = user\n ? await this.passwordService.verify(dto.password, user.password)\n : false;\n\n // ✅ Log failed attempts (but not passwords!)\n if (!isValid) {\n await this.auditService.log({\n event: 'LOGIN_FAILED',\n email: dto.email,\n ip: dto.ip,\n userAgent: dto.userAgent,\n });\n }\n\n // ... rest of logic\n }\n}\n```\n\n## Best Practices Summary\n\n| Practice | Why |\n|----------|-----|\n| Use `Bun.password.hash()` | Argon2/bcrypt with runtime algorithm selection |\n| Use `Bun.password.verify()` | Auto-detects algorithm, timing-safe comparison |\n| Prefer argon2 for new apps | Memory-hard, GPU-resistant, modern standard |\n| Use bcrypt for compatibility | Legacy systems, cross-platform needs |\n| Configure algorithm at runtime | Environment-specific, flexible migration |\n| Enforce strong passwords | Prevents brute force attacks |\n| Generic error messages | Prevents user enumeration |\n| Never log passwords | Logs can be compromised |\n| Rehash on algorithm upgrade | Seamless migration between algorithms |\n| Rate limit auth endpoints | Prevents brute force attacks |\n\n**Sources:**\n- [Bun Crypto Documentation](https://bun.sh/docs/api/crypto)\n- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)\n- [Argon2 RFC](https://datatracker.ietf.org/doc/html/rfc9106)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22387,"content_sha256":"7296c0b260291d1752bb2cda54ea6b50372a7d1e461b15ef51c088e00cd57ca6"},{"filename":"rules/auth-route-guards.md","content":"---\ntitle: Use Guards for Route Protection\nimpact: CRITICAL\nsection: 7\nimpactDescription: Enforces authentication/authorization per route\ntags: security, guards, auth, authorization\n---\n\nUnprotected routes expose sensitive data. Guards run before controllers and can short-circuit requests. **Protect endpoints explicitly.**\n\n> **Hint**: Guards determine whether a request will be handled by the controller or not. Use them for authentication (who are you?) and authorization (what can you do?). Always use global guards with public route decorators for default-deny security.\n\n## For AI Agents\n\nWhen implementing or reviewing security, **always** follow these steps:\n\n### Step 1: Set Up Global Authentication Guard\n**Files to create/modify:**\n- `src/auth/guards/jwt-auth.guard.ts`\n- `src/auth/guards/jwt-auth.guard.spec.ts`\n- `src/auth/decorators/public.decorator.ts`\n- `src/app.module.ts` (for APP_GUARD)\n\n```typescript\n// ✅ REQUIRED: Global JWT Guard\nimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { JwtService } from '@nestjs/jwt';\n\n@Injectable()\nexport class JwtAuthGuard implements CanActivate {\n constructor(\n private jwtService: JwtService,\n private reflector: Reflector,\n ) {}\n\n canActivate(context: ExecutionContext): boolean {\n const isPublic = this.reflector.getAllAndOverride\u003cboolean>(\n 'isPublic',\n [context.getHandler(), context.getClass()],\n );\n\n if (isPublic) return true;\n\n const request = context.switchToHttp().getRequest();\n const token = this.extractTokenFromHeader(request);\n\n if (!token) return false;\n\n try {\n const payload = this.jwtService.verify(token);\n request['user'] = payload;\n } catch {\n return false;\n }\n\n return true;\n }\n\n private extractTokenFromHeader(request: Request): string | undefined {\n const [type, token] = request.headers.authorization?.split(' ') ?? [];\n return type === 'Bearer' ? token : undefined;\n }\n}\n```\n\n### Step 2: Create @Public() Decorator\n**File:** `src/auth/decorators/public.decorator.ts`\n\n```typescript\n// ✅ REQUIRED: For marking public routes\nimport { SetMetadata } from '@nestjs/common';\n\nexport const IS_PUBLIC_KEY = 'isPublic';\nexport const Public = () => SetMetadata(IS_PUBLIC_KEY, true);\n```\n\n### Step 3: Register Guard Globally\n**File:** `src/app.module.ts`\n\n```typescript\n// ✅ REQUIRED: Global guard registration\nimport { APP_GUARD } from '@nestjs/core';\n\n@Module({\n providers: [\n {\n provide: APP_GUARD,\n useClass: JwtAuthGuard, // ✅ Global authentication\n },\n ],\n})\nexport class AppModule {}\n```\n\n### Step 4: Check All Controllers Have Proper Protection\n**Pattern to check:**\n\n```typescript\n// ❌ WRONG - No protection\n@Controller('users')\nexport class UsersController {\n @Get()\n findAll() { } // Anyone can access!\n}\n\n// ❌ WRONG - Manual check in controller\n@Controller('users')\nexport class UsersController {\n @Get()\n findAll(@Req() req) {\n if (!req.user) throw new UnauthorizedException(); // Too late!\n }\n}\n\n// ✅ CORRECT - Protected by global guard\n@Controller('users')\nexport class UsersController {\n @Get()\n findAll() { } // Protected by JwtAuthGuard\n}\n\n// ✅ CORRECT - Explicitly public\n@Controller('auth')\nexport class AuthController {\n @Post('login')\n @Public() // ✅ Marked as public\n login() { }\n}\n```\n\n## Quick Reference Checklist\n\nUse this checklist when reviewing or creating endpoints:\n\n- [ ] Global authentication guard registered in `app.module.ts`\n- [ ] `@Public()` decorator exists for public routes\n- [ ] Controllers don't use `any` type for `@Req()` user\n- [ ] Admin routes have `@Roles('admin')` guard\n- [ ] Resource owner checks implemented (users can only access their own data)\n- [ ] Rate limiting applied to auth endpoints\n- [ ] Guards use `Reflector` to check metadata\n\n## Guard Execution Order\n\n```\nRequest → Middleware → Guards → Interceptors → Pipes → Controller\n ↑\n Short-circuit here\n```\n\n## Installation\n\n```bash\nbun add @nestjs/jwt @nestjs/passport passport passport-jwt\nbun add -D @types/passport-jwt\n```\n\n**Incorrect:**\n\n```typescript\n// users.controller.ts - All routes exposed 🚨\n@Controller('users')\nexport class UsersController {\n @Get()\n findAll() {\n return this.usersService.findAll(); // 🚨 Publicly accessible!\n }\n\n @Get(':id')\n findOne(@Param('id') id: string) {\n return this.usersService.findOne(id); // 🚨 Anyone can view any user!\n }\n\n @Delete(':id')\n remove(@Param('id') id: string) {\n return this.usersService.remove(id); // 🚨 Anyone can delete users!\n }\n}\n```\n\n**Correct:**\n\n```typescript\n// auth/guards/jwt-auth.guard.ts\nimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { JwtService } from '@nestjs/jwt';\n\n@Injectable()\nexport class JwtAuthGuard implements CanActivate {\n constructor(\n private jwtService: JwtService,\n private reflector: Reflector,\n ) {}\n\n canActivate(context: ExecutionContext): boolean {\n const isPublic = this.reflector.getAllAndOverride\u003cboolean>(\n 'isPublic',\n [context.getHandler(), context.getClass()],\n );\n\n if (isPublic) return true; // ✅ Skip for public routes\n\n const request = context.switchToHttp().getRequest();\n const token = this.extractTokenFromHeader(request);\n\n if (!token) return false;\n\n try {\n const payload = this.jwtService.verify(token);\n request['user'] = payload; // Attach user to request\n } catch {\n return false;\n }\n\n return true;\n }\n\n private extractTokenFromHeader(request: Request): string | undefined {\n const [type, token] = request.headers.authorization?.split(' ') ?? [];\n return type === 'Bearer' ? token : undefined;\n }\n}\n\n// auth/decorators/public.decorator.ts\nimport { SetMetadata } from '@nestjs/common';\n\nexport const IS_PUBLIC_KEY = 'isPublic';\nexport const Public = () => SetMetadata(IS_PUBLIC_KEY, true);\n\n// app.module.ts - Global guard registration\nimport { APP_GUARD } from '@nestjs/core';\n\n@Module({\n providers: [\n {\n provide: APP_GUARD,\n useClass: JwtAuthGuard, // ✅ All routes protected by default\n },\n ],\n})\nexport class AppModule {}\n\n// auth/auth.controller.ts - Public routes\n@Controller('auth')\nexport class AuthController {\n @Post('login')\n @Public() // ✅ Explicitly mark as public\n async login(@Body() loginDto: LoginDto) {\n return this.authService.login(loginDto);\n }\n\n @Post('register')\n @Public() // ✅ Explicitly mark as public\n async register(@Body() registerDto: RegisterDto) {\n return this.authService.register(registerDto);\n }\n}\n\n// users/users.controller.ts - Protected by default\n@Controller('users')\nexport class UsersController {\n @Get()\n findAll() {\n // ✅ Protected by global guard - no @UseGuards needed\n return this.usersService.findAll();\n }\n\n @Get('profile')\n getProfile(@Req() request: Request) {\n // ✅ User attached by guard\n return request['user'];\n }\n}\n```\n\n## Role-Based Authorization\n\n```typescript\n// auth/guards/roles.guard.ts\nimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\n\n@Injectable()\nexport class RolesGuard implements CanActivate {\n constructor(private reflector: Reflector) {}\n\n canActivate(context: ExecutionContext): boolean {\n const requiredRoles = this.reflector.get\u003cstring[]>(\n 'roles',\n context.getHandler(),\n );\n\n if (!requiredRoles) return true;\n\n const { user } = context.switchToHttp().getRequest();\n return requiredRoles.some((role) => user.roles?.includes(role));\n }\n}\n\n// auth/decorators/roles.decorator.ts\nimport { SetMetadata } from '@nestjs/common';\n\nexport const Roles = (...roles: string[]) => SetMetadata('roles', roles);\n\n// admin/admin.controller.ts\n@Controller('admin')\n@UseGuards(RolesGuard) // ✅ Apply to controller\nexport class AdminController {\n @Get()\n @Roles('admin') // ✅ Admins only\n getDashboard() {\n return this.adminService.getDashboard();\n }\n\n @Delete('users/:id')\n @Roles('admin') // ✅ Only admins can delete\n deleteUser(@Param('id') id: string) {\n return this.adminService.deleteUser(id);\n }\n}\n```\n\n## Resource Owner Guard\n\n```typescript\n// auth/guards/owner.guard.ts\nimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';\n\n@Injectable()\nexport class OwnerGuard implements CanActivate {\n canActivate(context: ExecutionContext): boolean {\n const { user, params } = context.switchToHttp().getRequest();\n\n // Admin can access everything\n if (user.roles?.includes('admin')) return true;\n\n // Users can only access their own resources\n return user.id === params.userId || user.id === params.id;\n }\n}\n\n// orders/orders.controller.ts\n@Controller('orders')\nexport class OrdersController {\n @Get('my-orders')\n @UseGuards(OwnerGuard) // ✅ Resource owner check\n getMyOrders(@Req() request: Request) {\n return this.ordersService.findByUserId(request['user'].id);\n }\n\n @Patch(':id')\n @UseGuards(OwnerGuard) // ✅ Users can only update their own orders\n update(@Param('id') id: string, @Body() dto: UpdateOrderDto) {\n return this.ordersService.update(id, dto);\n }\n}\n```\n\n## Permission-Based Authorization\n\n```typescript\n// auth/decorators/require-permissions.decorator.ts\nexport const REQUIRE_PERMISSIONS_KEY = 'requirePermissions';\nexport const RequirePermissions = (...permissions: string[]) =>\n SetMetadata(REQUIRE_PERMISSIONS_KEY, permissions);\n\n// auth/guards/permissions.guard.ts\n@Injectable()\nexport class PermissionsGuard implements CanActivate {\n constructor(private reflector: Reflector) {}\n\n canActivate(context: ExecutionContext): boolean {\n const requiredPermissions = this.reflector.get\u003cstring[]>(\n REQUIRE_PERMISSIONS_KEY,\n context.getHandler(),\n );\n\n if (!requiredPermissions) return true;\n\n const { user } = context.switchToHttp().getRequest();\n return requiredPermissions.every((p) => user.permissions?.includes(p));\n }\n}\n\n// products/products.controller.ts\n@Controller('products')\n@UseGuards(PermissionsGuard)\nexport class ProductsController {\n @Post()\n @RequirePermissions('product:create') // ✅ Requires specific permission\n create(@Body() dto: CreateProductDto) {\n return this.productsService.create(dto);\n }\n\n @Delete(':id')\n @RequirePermissions('product:delete') // ✅ Requires specific permission\n remove(@Param('id') id: string) {\n return this.productsService.remove(id);\n }\n}\n```\n\n## API Key Guard (Service-to-Service)\n\n```typescript\n// auth/guards/api-key.guard.ts\nimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\n\n@Injectable()\nexport class ApiKeyGuard implements CanActivate {\n constructor(private configService: ConfigService) {}\n\n canActivate(context: ExecutionContext): boolean {\n const request = context.switchToHttp().getRequest();\n const apiKey = request.headers['x-api-key'];\n\n const validApiKeys = this.configService.get\u003cstring>('API_KEYS')?.split(',') || [];\n return validApiKeys.includes(apiKey);\n }\n}\n\n// webhook/webhook.controller.ts\n@Controller('webhook')\n@UseGuards(ApiKeyGuard) // ✅ Requires valid API key\nexport class WebhookController {\n @Post('stripe')\n handleStripeWebhook(@Body() payload: any) {\n return this.webhookService.handleStripeEvent(payload);\n }\n}\n```\n\n## Rate Limiting Auth Endpoints\n\n```typescript\n// First install: bun add @nestjs/throttler\n\n// app.module.ts\nimport { ThrottlerModule } from '@nestjs/throttler';\n\n@Module({\n imports: [\n ThrottlerModule.forRoot([{\n ttl: 60000, // 1 minute\n limit: 5, // 5 requests per minute\n }]),\n ],\n})\nexport class AppModule {}\n\n// auth/auth.controller.ts\nimport { Throttle } from '@nestjs/throttler';\n\n@Controller('auth')\nexport class AuthController {\n @Post('login')\n @Throttle({ default: { limit: 5, ttl: 60000 } }) // ✅ Stricter for login\n login(@Body() dto: LoginDto) {\n return this.authService.login(dto);\n }\n\n @Post('register')\n @Throttle({ default: { limit: 3, ttl: 3600000 } }) // ✅ 3 per hour\n register(@Body() dto: RegisterDto) {\n return this.authService.register(dto);\n }\n}\n```\n\n## Summary: Guard Best Practices\n\n| Practice | Description |\n|----------|-------------|\n| Use global guards | Default-deny security with explicit public routes |\n| Separate auth from authorization | Different guards for authentication vs authorization |\n| Use decorators | Custom decorators improve code readability |\n| Check resource ownership | Users can only access their own resources |\n| Combine guards | Chain multiple guards for comprehensive security |\n| Rate limit auth endpoints | Prevent brute force attacks |\n\n**Sources:**\n- [Guards | NestJS - Official Documentation](https://docs.nestjs.com/guards)\n- [Authentication | NestJS - Official Documentation](https://docs.nestjs.com/security/authentication)\n- [Authorization | NestJS - Official Documentation](https://docs.nestjs.com/security/authorization)\n- [Throttler | NestJS Documentation](https://docs.nestjs.com/security/rate-limiting)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13226,"content_sha256":"8bd25168bca99df19dd6942558feaf4b9ebc896a627aff816a202012271ec62d"},{"filename":"rules/config-no-secrets.md","content":"---\ntitle: Never Hardcode Secrets - Use Environment Variables\nimpact: CRITICAL\nimpactDescription: Prevents credential leaks in source control\nsection: 9\ntags: security, config, environment, secrets\n---\n\n## Never Hardcode Secrets - Use Environment Variables\n\nHardcoded credentials in source code get committed to git and exposed publicly. The `@nestjs/config` package provides a secure way to manage configuration through environment variables. **Secrets belong in environment only.**\n\n### Installation\n\n```bash\nbun add @nestjs/config\n```\n\n**Incorrect:**\n\n```typescript\n// database.module.ts\nTypeOrmModule.forRoot({\n url: 'postgres://user:password@localhost/db', // 🚨 Exposed!\n ssl: {\n cert: process.env.DB_CERT, // Still vulnerable if cert is committed\n },\n})\n\n// auth.service.ts\n@Injectable()\nexport class AuthService {\n private readonly jwtSecret = 'my-super-secret-key-12345'; // 🚨 Exposed!\n private readonly apiKey = 'sk_test_abc123xyz'; // 🚨 Exposed!\n}\n\n// payment.controller.ts\nconst stripe = require('stripe')('sk_test_51ABC...'); // 🚨 Exposed!\n```\n\n**Correct:**\n\n```typescript\n// app.module.ts\nimport { ConfigModule } from '@nestjs/config';\n\n@Module({\n imports: [\n ConfigModule.forRoot({\n isGlobal: true,\n envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,\n cache: true,\n }),\n ],\n})\nexport class AppModule {}\n\n// database.module.ts\nimport { ConfigService } from '@nestjs/config';\n\nTypeOrmModule.forRootAsync({\n imports: [ConfigModule],\n inject: [ConfigService],\n useFactory: (configService: ConfigService) => ({\n url: configService.getOrThrow('DATABASE_URL'), // ✅ Secure\n ssl: configService.get('DB_SSL') === 'true' ? {\n cert: configService.get('DB_CERT'),\n } : false,\n }),\n})\n\n// auth.service.ts\n@Injectable()\nexport class AuthService {\n constructor(private configService: ConfigService) {}\n\n private get jwtSecret(): string {\n return this.configService.getOrThrow('JWT_SECRET'); // ✅ Secure\n }\n}\n\n// payment.module.ts\nimport { ConfigModule, ConfigService } from '@nestjs/config';\n\n@Module({\n imports: [\n StripeModule.forRootAsync({\n imports: [ConfigModule],\n inject: [ConfigService],\n useFactory: (configService: ConfigService) => ({\n apiKey: configService.getOrThrow('STRIPE_SECRET_KEY'), // ✅ Secure\n webhookSecret: configService.get('STRIPE_WEBHOOK_SECRET'),\n }),\n }),\n ],\n})\nexport class PaymentModule {}\n```\n\n## Type-Safe Configuration with Validation\n\nUse Joi or Zod to validate environment variables at startup:\n\n```typescript\n// app.module.ts\nimport * as Joi from 'joi';\n\nConfigModule.forRoot({\n isGlobal: true,\n validationSchema: Joi.object({\n // Database\n DATABASE_URL: Joi.string().required(),\n DB_HOST: Joi.string().required(),\n DB_PORT: Joi.number().default(5432),\n DB_USERNAME: Joi.string().required(),\n DB_PASSWORD: Joi.string().required(),\n\n // JWT\n JWT_SECRET: Joi.string().min(32).required(),\n JWT_EXPIRES_IN: Joi.string().default('1d'),\n\n // API Keys\n STRIPE_SECRET_KEY: Joi.string().when('NODE_ENV', {\n is: 'production',\n then: Joi.required(),\n otherwise: Joi.optional(),\n }),\n }),\n validationOptions: {\n abortEarly: false, // Show all errors\n allowUnknown: true,\n },\n})\n```\n\n## Custom Configuration with Namespaces\n\nUse `registerAs` for type-safe, grouped configuration:\n\n```typescript\n// config/database.config.ts\nimport { registerAs } from '@nestjs/config';\n\nexport default registerAs('database', () => ({\n host: process.env.DB_HOST || 'localhost',\n port: parseInt(process.env.DB_PORT, 10) || 5432,\n username: process.env.DB_USERNAME,\n password: process.env.DB_PASSWORD,\n database: process.env.DB_DATABASE,\n}));\n\n// config/jwt.config.ts\nexport default registerAs('jwt', () => ({\n secret: process.env.JWT_SECRET,\n expiresIn: process.env.JWT_EXPIRES_IN || '1d',\n refreshSecret: process.env.JWT_REFRESH_SECRET,\n refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',\n}));\n\n// config/index.ts\nimport databaseConfig from './database.config';\nimport jwtConfig from './jwt.config';\n\nexport default [databaseConfig, jwtConfig];\n\n// app.module.ts\nConfigModule.forRoot({\n isGlobal: true,\n load: [config],\n})\n\n// Using in services\n@Injectable()\nexport class MyService {\n constructor(private configService: ConfigService) {}\n\n getDbHost(): string {\n return this.configService.get\u003cstring>('database.host');\n }\n\n getJwtSecret(): string {\n return this.configService.getOrThrow('jwt.secret');\n }\n}\n```\n\n## Environment-Specific Configuration\n\n```bash\n# .env.development\nNODE_ENV=development\nDATABASE_URL=postgres://localhost:5432/dev_db\nJWT_SECRET=dev-secret-key\n\n# .env.production\nNODE_ENV=production\nDATABASE_URL=postgres://prod-server:5432/prod_db\nJWT_SECRET=${PROD_JWT_SECRET}\n\n# .env.test\nNODE_ENV=test\nDATABASE_URL=postgres://localhost:5432/test_db\nJWT_SECRET=test-secret-key\n```\n\n## .env.example Template\n\nAlways provide a `.env.example` file to document required variables:\n\n```bash\n# Application\nNODE_ENV=development|production|test\nPORT=3000\n\n# Database (Required)\nDATABASE_URL=\nDB_HOST=localhost\nDB_PORT=5432\nDB_USERNAME=\nDB_PASSWORD=\nDB_DATABASE=\n\n# JWT (Required - min 32 characters)\nJWT_SECRET=\nJWT_EXPIRES_IN=1d\nJWT_REFRESH_SECRET=\nJWT_REFRESH_EXPIRES_IN=7d\n\n# API Keys (Optional)\nSTRIPE_SECRET_KEY=\nSTRIPE_WEBHOOK_SECRET=\nSENDGRID_API_KEY=\n```\n\n## Production Considerations\n\n```typescript\n// For production, ignore .env files and use process.env only\nConfigModule.forRoot({\n isGlobal: true,\n ignoreEnvFile: process.env.NODE_ENV === 'production',\n validationSchema: productionValidationSchema,\n})\n```\n\n**Sources:**\n- [Configuration | NestJS - Official Documentation](https://docs.nestjs.com/techniques/configuration)\n- [Managing Environment Variables in NestJS with ConfigModule | Medium](https://medium.com/@hashirmughal1000/managing-environment-variables-in-nestjs-with-configmodule-5b0742efb69c)\n- [Managing Configuration and Environment Variables in NestJS | dev.to](https://dev.to/vishnucprasad/managing-configuration-and-environment-variables-in-nestjs-28ni)\n- [NestJS Environment Configuration Using Zod | Medium](https://medium.com/@rotemdoar17/nestjs-environment-configuration-using-zod-92e3decca5ca)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6271,"content_sha256":"126adb7a8cbc31b31d689ec4740fb9f591c49f52f03416ef0cbebc645202f64a"},{"filename":"rules/database-parameterized-queries.md","content":"---\ntitle: Use Parameterized Queries to Prevent SQL Injection\nimpact: CRITICAL\nsection: 6\nimpactDescription: Eliminates SQL injection vulnerabilities\ntags: security, database, sql-injection, prisma\n---\n\nRaw SQL queries with string concatenation allow attackers to inject malicious code. Prisma Client automatically parameterizes all queries, preventing injection. **Never use string concatenation with user input.**\n\n> **Hint**: Prisma v7 introduces the new `sql` template tag for raw queries. Always use tagged template literals instead of string concatenation. Prisma automatically parameterizes all variables passed to template tags.\n\n## For AI Agents\n\nWhen implementing or reviewing database queries, **always** follow these steps:\n\n### Step 1: Check for Raw SQL Usage\n**Pattern to check:** Look for `$queryRaw`, `$executeRaw`, or `sql` template tag usage.\n\n```typescript\n// ❌ WRONG - String concatenation\nconst query = `SELECT * FROM users WHERE id = '${userId}'`;\nawait prisma.$queryRawUnsafe(query);\n\n// ❌ WRONG - String concatenation with template literal\nconst query = `SELECT * FROM users WHERE name = '${name}' AND status = '${status}'`;\nawait prisma.$executeRawUnsafe(query);\n\n// ✅ CORRECT - Prisma v7 sql template tag (auto-parameterized)\nimport { sql } from '@prisma/client/runtime/library';\n\nconst result = await prisma.$queryRaw`\n SELECT * FROM users\n WHERE id = ${userId}\n`;\n```\n\n**If found:** Replace with Prisma v7 `sql` template tag or typed queries.\n\n### Step 2: Verify Prisma v7 Syntax for Raw Queries\n**File:** Any service using Prisma\n\n```typescript\n// ✅ REQUIRED: Prisma v7 pattern with sql tag\nimport { sql } from '@prisma/client/runtime/library';\n\n// Single parameter\nconst users = await prisma.$queryRaw`\n SELECT * FROM users\n WHERE email = ${email}\n`;\n\n// Multiple parameters\nconst results = await prisma.$queryRaw`\n SELECT * FROM orders\n WHERE user_id = ${userId}\n AND status = ${status}\n AND created_at > ${since}\n`;\n\n// With JOIN\nconst userWithOrders = await prisma.$queryRaw`\n SELECT u.*, COUNT(o.id) as order_count\n FROM users u\n LEFT JOIN orders o ON o.user_id = u.id\n WHERE u.id = ${userId}\n GROUP BY u.id\n`;\n```\n\n### Step 3: Check Transaction Syntax\n**Prisma v7 transaction pattern:**\n\n```typescript\n// ❌ WRONG - Old transaction syntax (v6 and below)\nawait prisma.$transaction([\n prisma.user.create({ data: userData }),\n prisma.post.create({ data: postData }),\n]);\n\n// ✅ CORRECT - Prisma v7 interactive transactions\nawait prisma.$transaction(async (tx) => {\n const user = await tx.user.create({ data: userData });\n await tx.post.create({ data: { ...postData, userId: user.id } });\n});\n```\n\n### Step 4: Verify Client Extension Patterns\n**File:** `src/database/prisma.service.ts` or similar\n\n```typescript\n// ✅ CORRECT: Prisma v7 client extension for custom queries\nimport { PrismaClient } from '@prisma/client';\n\nconst prisma = new PrismaClient().$extends({\n query: {\n $allOperations({ operation, args, query }) {\n // Log all queries\n console.log(`[Prisma] ${operation}`, args);\n return query(args);\n },\n },\n});\n\n// Extended client with custom methods\nconst extendedPrisma = new PrismaClient().$extends({\n model: {\n user: {\n async findByEmail(email: string) {\n return prisma.user.findFirst({ where: { email } });\n },\n },\n },\n});\n```\n\n## Installation\n\n```bash\nbun add @prisma/client @prisma/adapter-pg\nbun add -D prisma\n```\n\n## Quick Reference Checklist\n\nUse this checklist when reviewing or creating database queries:\n\n- [ ] No string concatenation in SQL queries\n- [ ] All `$queryRaw` uses template literals with variables\n- [ ] All `$executeRaw` uses template literals with variables\n- [ ] Prisma v7 `sql` template tag imported when needed\n- [ ] Transactions use async callback pattern\n- [ ] Client extensions use `$extends()` method\n- [ ] User input never interpolated directly into SQL\n\n**Incorrect:**\n\n```typescript\n// users.service.ts - DANGEROUS 🚨\n\nimport { Injectable } from '@nestjs/common';\nimport { PrismaService } from './prisma.service';\n\n@Injectable()\nexport class UsersService {\n constructor(private prisma: PrismaService) {}\n\n // ❌ String concatenation - SQL INJECTION\n async findByName(name: string) {\n const query = `SELECT * FROM users WHERE name = '${name}'`;\n return this.prisma.$queryRawUnsafe(query);\n }\n\n // ❌ Template literal concatenation - VULNERABLE\n async findByEmailAndPassword(email: string, password: string) {\n const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;\n return this.prisma.$executeRawUnsafe(query);\n }\n\n // ❌ Direct interpolation - VULNERABLE\n async searchUsers(searchTerm: string) {\n return this.prisma.$queryRaw(\n `SELECT * FROM users WHERE name LIKE '%${searchTerm}%'`\n );\n }\n\n // ❌ Old Prisma syntax (pre-v7)\n async oldTransactionPattern(userData: any, postData: any) {\n return this.prisma.$transaction([\n this.prisma.user.create({ data: userData }),\n this.prisma.post.create({ data: postData }),\n ]);\n }\n}\n```\n\n**Correct:**\n\n```typescript\n// users.service.ts - SAFE ✅\n\nimport { Injectable } from '@nestjs/common';\nimport { PrismaService } from './prisma.service';\nimport { sql } from '@prisma/client/runtime/library';\n\n@Injectable()\nexport class UsersService {\n constructor(private prisma: PrismaService) {}\n\n // ✅ Typed query - Safest option\n async findByName(name: string) {\n return this.prisma.user.findMany({\n where: { name },\n });\n }\n\n // ✅ Raw query with parameterization\n async findByEmail(email: string) {\n return this.prisma.$queryRaw`\n SELECT * FROM users\n WHERE email = ${email}\n `;\n }\n\n // ✅ Multiple parameters - All auto-parameterized\n async findByEmailAndStatus(email: string, status: string) {\n return this.prisma.$queryRaw`\n SELECT * FROM users\n WHERE email = ${email}\n AND status = ${status}\n `;\n }\n\n // ✅ Parameterized LIKE query\n async searchUsers(searchTerm: string) {\n return this.prisma.$queryRaw`\n SELECT * FROM users\n WHERE name LIKE ${`%${searchTerm}%`}\n `;\n }\n\n // ✅ Prisma v7 transaction pattern\n async createUserWithPost(userData: any, postData: any) {\n return this.prisma.$transaction(async (tx) => {\n const user = await tx.user.create({ data: userData });\n const post = await tx.post.create({\n data: { ...postData, userId: user.id },\n });\n return { user, post };\n });\n }\n\n // ✅ Complex JOIN with parameterization\n async getUserWithOrderCount(userId: string) {\n return this.prisma.$queryRaw`\n SELECT\n u.id,\n u.name,\n u.email,\n COUNT(o.id) as order_count,\n SUM(o.total) as total_spent\n FROM users u\n LEFT JOIN orders o ON o.user_id = u.id\n WHERE u.id = ${userId}\n GROUP BY u.id, u.name, u.email\n `;\n }\n\n // ✅ Execute raw with parameterization\n async bulkUpdateStatus(userIds: string[], newStatus: string) {\n return this.prisma.$executeRaw`\n UPDATE users\n SET status = ${newStatus}, updated_at = NOW()\n WHERE id = ANY(${userIds})\n `;\n }\n}\n```\n\n## Prisma v7 Raw Query Examples\n\n### Basic SELECT Queries\n\n```typescript\nimport { sql } from '@prisma/client/runtime/library';\n\n// Simple WHERE\nconst users = await prisma.$queryRaw`\n SELECT * FROM users\n WHERE status = ${status}\n AND created_at > ${since}\n`;\n\n// IN clause with array\nconst activeUsers = await prisma.$queryRaw`\n SELECT * FROM users\n WHERE id IN (${[userId1, userId2, userId3]})\n`;\n\n// ORDER BY with dynamic column (use sql tag for identifiers)\nconst users = await prisma.$queryRaw`\n SELECT * FROM users\n ORDER BY ${sql('created_at')} ${sortOrder === 'asc' ? sql('ASC') : sql('DESC')}\n LIMIT ${limit} OFFSET ${offset}\n`;\n```\n\n### INSERT, UPDATE, DELETE\n\n```typescript\n// INSERT with returning\nconst newUser = await prisma.$queryRaw`\n INSERT INTO users (name, email, status)\n VALUES (${name}, ${email}, ${status})\n RETURNING *\n`;\n\n// UPDATE\nconst updated = await prisma.$executeRaw`\n UPDATE users\n SET status = ${newStatus}, updated_at = NOW()\n WHERE id = ${userId}\n`;\n\n// DELETE with returning\nconst deleted = await prisma.$queryRaw`\n DELETE FROM sessions\n WHERE expires_at \u003c NOW()\n RETURNING *\n`;\n```\n\n### Complex Queries\n\n```typescript\n// Aggregation\nconst stats = await prisma.$queryRaw`\n SELECT\n DATE(created_at) as date,\n COUNT(*) as count,\n SUM(amount) as total\n FROM orders\n WHERE created_at >= ${startDate}\n AND created_at \u003c= ${endDate}\n GROUP BY DATE(created_at)\n ORDER BY date DESC\n`;\n\n// CTE (Common Table Expression)\nconst paginatedResults = await prisma.$queryRaw`\n WITH user_orders AS (\n SELECT\n u.id,\n u.name,\n u.email,\n COUNT(o.id) as order_count,\n ROW_NUMBER() OVER (ORDER BY COUNT(o.id) DESC) as rn\n FROM users u\n LEFT JOIN orders o ON o.user_id = u.id\n GROUP BY u.id, u.name, u.email\n )\n SELECT * FROM user_orders\n WHERE rn BETWEEN ${offset + 1} AND ${offset + limit}\n`;\n\n// UNION\nconst allItems = await prisma.$queryRaw`\n SELECT id, name, 'product' as type FROM products\n WHERE name ILIKE ${`%${search}%`}\n UNION\n SELECT id, name, 'category' as type FROM categories\n WHERE name ILIKE ${`%${search}%`}\n`;\n```\n\n## Prisma v7 Transaction Patterns\n\n### Interactive Transactions\n\n```typescript\n// Single operation transaction\nawait prisma.$transaction(async (tx) => {\n const account = await tx.account.update({\n where: { id: accountId },\n data: { balance: { decrement: amount } },\n });\n\n if (account.balance \u003c 0) {\n throw new Error('Insufficient funds');\n }\n\n await tx.transaction.create({\n data: {\n accountId,\n amount,\n type: 'WITHDRAWAL',\n },\n });\n});\n\n// Multiple related operations\nawait prisma.$transaction(async (tx) => {\n // Create order\n const order = await tx.order.create({\n data: {\n userId,\n total: cartTotal,\n status: 'PENDING',\n },\n });\n\n // Create order items\n await tx.orderItem.createMany({\n data: cartItems.map(item => ({\n orderId: order.id,\n productId: item.productId,\n quantity: item.quantity,\n price: item.price,\n })),\n });\n\n // Update inventory\n for (const item of cartItems) {\n await tx.product.update({\n where: { id: item.productId },\n data: { stock: { decrement: item.quantity } },\n });\n }\n\n return order;\n});\n```\n\n### Transaction Options\n\n```typescript\n// With timeout\nawait prisma.$transaction(\n async (tx) => {\n // Operations here\n },\n {\n maxWait: 5000, // Max time to wait for transaction\n timeout: 10000, // Max time for transaction execution\n }\n);\n```\n\n## Client Extensions (Prisma v7)\n\n### Logging Extension\n\n```typescript\n// prisma.service.ts\nimport { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';\nimport { PrismaClient } from '@prisma/client';\n\n@Injectable()\nexport class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {\n constructor() {\n super({\n log: ['query', 'info', 'warn', 'error'],\n });\n\n // Add query logging extension\n this.$extends({\n query: {\n $allOperations({ operation, model, args, query }) {\n const start = Date.now();\n const result = query(args);\n const end = Date.now();\n console.log(`[Prisma] ${model}.${operation} took ${end - start}ms`);\n return result;\n },\n },\n });\n }\n\n async onModuleInit() {\n await this.$connect();\n }\n\n async onModuleDestroy() {\n await this.$disconnect();\n }\n}\n```\n\n### Model Extensions\n\n```typescript\n// Extended client with custom methods\nconst prisma = new PrismaClient().$extends({\n model: {\n user: {\n async findByEmail(email: string) {\n return prisma.user.findUnique({ where: { email } });\n },\n async findActive() {\n return prisma.user.findMany({ where: { status: 'ACTIVE' } });\n },\n },\n order: {\n async calculateTotal(orderId: string) {\n const items = await prisma.orderItem.findMany({\n where: { orderId },\n });\n return items.reduce((sum, item) => sum + item.price * item.quantity, 0);\n },\n },\n },\n});\n\n// Usage\nconst user = await prisma.user.findByEmail('[email protected]');\nconst total = await prisma.order.calculateTotal('order-123');\n```\n\n## Best Practices Summary\n\n| Practice | Why |\n|----------|-----|\n| Use typed queries | Type-safe, auto-parameterized |\n| Avoid raw queries when possible | Prisma handles security automatically |\n| Use `sql` template tag | Auto-parameterizes variables |\n| Never concatenate strings | Prevents SQL injection |\n| Use interactive transactions | Better error handling in v7 |\n| Use client extensions | Reusable query patterns |\n\n**Sources:**\n- [Prisma v7 Release Notes](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference)\n- [Raw Database Queries | Prisma Documentation](https://www.prisma.io/docs/orm/prisma-client/queries/raw-database-access)\n- [Transactions | Prisma Documentation](https://www.prisma.io/docs/orm/prisma-client/transactions)\n- [SQL Injection | OWASP](https://owasp.org/www-community/attacks/SQL_Injection)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13205,"content_sha256":"a59b3356dc81a573befb76894b9082b89739aece2d837a12b4606a22af8ae759"},{"filename":"rules/database-repository-pattern.md","content":"---\ntitle: Use Custom Repository Pattern for Database Logic Encapsulation\nimpact: HIGH\nimpactDescription: Separates concerns and improves testability\nsection: 6\ntags: database, repository, typeorm, encapsulation, clean-architecture\n---\n\nPlacing database query logic directly in services leads to bloated service classes, mixing business logic with data access, and difficult testing. Custom repositories extend the base TypeORM repository and encapsulate all database logic, keeping services clean and focused on business rules.\n\n## For AI Agents\n\nWhen implementing or reviewing database operations, **always** follow these steps:\n\n### Step 1: Check for Database Logic in Services\n**Pattern to check:** Look for `@InjectRepository()`, query builders, or database methods (find, save, update, delete) in service files.\n\n```typescript\n// ❌ WRONG - Database logic in service\n@Injectable()\nexport class TasksService {\n constructor(\n @InjectRepository(Task) private repo: Repository\u003cTask>\n ) {}\n\n async getTasks(user: User, filterDto: GetTasksFilterDto) {\n // ❌ Database query logic in service\n const { status, search } = filterDto;\n const query = this.repo.createQueryBuilder('task');\n query.where({ user });\n\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n return await query.getMany();\n }\n\n async findOne(id: string, user: User) {\n // ❌ Repeated query logic\n return this.repo.findOne({ where: { id, user } });\n }\n}\n```\n\n**If found:** Extract to custom repository.\n\n### Step 2: Create Custom Repository\n**File:** `tasks/tasks.repository.ts`\n\n```typescript\n// ✅ REQUIRED: Custom repository extending TypeORM Repository\nimport { EntityRepository, Repository } from 'typeorm';\nimport { Task } from './task.entity';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\nimport { User } from '../auth/entities/user.entity';\n\n// For TypeORM \u003c 0.3\n@EntityRepository(Task)\nexport class TasksRepository extends Repository\u003cTask> {\n async getTasks(filterDto: GetTasksFilterDto, user: User): Promise\u003cTask[]> {\n const { status, search } = filterDto;\n const query = this.createQueryBuilder('task');\n\n query.where({ user });\n\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n return await query.getMany();\n }\n\n async findOneById(id: string, user: User): Promise\u003cTask> {\n return this.findOne({ where: { id, user } });\n }\n}\n\n// For TypeORM >= 0.3 (using dataSource)\nimport { DataSource, Repository } from 'typeorm';\nimport { Task } from './task.entity';\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class TasksRepository {\n constructor(private dataSource: DataSource) {\n // Get repository from dataSource\n }\n\n private get repo(): Repository\u003cTask> {\n return this.dataSource.getRepository(Task);\n }\n\n async getTasks(filterDto: GetTasksFilterDto, user: User): Promise\u003cTask[]> {\n const { status, search } = filterDto;\n const query = this.repo.createQueryBuilder('task');\n\n query.where({ user });\n\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n return await query.getMany();\n }\n\n async findOneById(id: string, user: User): Promise\u003cTask> {\n return this.repo.findOne({ where: { id, user } });\n }\n}\n```\n\n### Step 3: Register Repository in Module\n**File:** `tasks/tasks.module.ts`\n\n```typescript\n// ✅ REQUIRED: Register custom repository\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { TasksController } from './tasks.controller';\nimport { TasksService } from './tasks.service';\nimport { TasksRepository } from './tasks.repository';\nimport { Task } from './task.entity';\n\n@Module({\n imports: [TypeOrmModule.forFeature([Task])],\n controllers: [TasksController],\n providers: [TasksService, TasksRepository],\n exports: [TasksService, TasksRepository],\n})\nexport class TasksModule {}\n```\n\n### Step 4: Inject Repository in Service\n**File:** `tasks/tasks.service.ts`\n\n```typescript\n// ✅ REQUIRED: Clean service with repository\n@Injectable()\nexport class TasksService {\n constructor(\n private tasksRepository: TasksRepository\n ) {}\n\n async getTasks(filterDto: GetTasksFilterDto, user: User): Promise\u003cTask[]> {\n // ✅ Delegates to repository\n return this.tasksRepository.getTasks(filterDto, user);\n }\n\n async findOne(id: string, user: User): Promise\u003cTask> {\n // ✅ Simple delegation\n return this.tasksRepository.findOneById(id, user);\n }\n\n async create(dto: CreateTaskDto, user: User): Promise\u003cTask> {\n // Service handles business rules\n const task = this.tasksRepository.create({\n ...dto,\n user,\n status: TaskStatus.OPEN,\n });\n return this.tasksRepository.save(task);\n }\n}\n```\n\n### Step 5: Add Complex Query Methods\n\n```typescript\n// ✅ OPTIONAL: Complex queries in repository\n// tasks/tasks.repository.ts\n@EntityRepository(Task)\nexport class TasksRepository extends Repository\u003cTask> {\n async getTasksWithStats(user: User): Promise\u003c{ tasks: Task[]; stats: any }> {\n const query = this.createQueryBuilder('task');\n\n query.where({ user });\n\n // ✅ Complex joins and aggregations\n query.leftJoinAndSelect('task.comments', 'comment');\n query.loadRelationCountAndMap('task.commentCount', 'task.comments');\n\n const tasks = await query.getMany();\n\n // ✅ Statistics calculation\n const stats = {\n total: tasks.length,\n byStatus: this.groupByStatus(tasks),\n };\n\n return { tasks, stats };\n }\n\n async findOverdueTasks(user: User): Promise\u003cTask[]> {\n return this.createQueryBuilder('task')\n .where('task.user = :user', { user })\n .andWhere('task.dueDate \u003c :now', { now: new Date() })\n .andWhere('task.status != :status', { status: TaskStatus.DONE })\n .orderBy('task.dueDate', 'ASC')\n .getMany();\n }\n\n private groupByStatus(tasks: Task[]): Record\u003cstring, number> {\n return tasks.reduce((acc, task) => {\n acc[task.status] = (acc[task.status] || 0) + 1;\n return acc;\n }, {} as Record\u003cstring, number>);\n }\n}\n```\n\n### Step 6: Add Transaction Support\n\n```typescript\n// ✅ OPTIONAL: Transaction methods\n// tasks/tasks.repository.ts\nimport { DataSource, QueryRunner } from 'typeorm';\n\nexport class TasksRepository extends Repository\u003cTask> {\n constructor(private dataSource: DataSource) {\n super();\n }\n\n async createWithComments(\n taskData: CreateTaskDto,\n comments: CreateCommentDto[],\n user: User,\n ): Promise\u003cTask> {\n const queryRunner = this.dataSource.createQueryRunner();\n await queryRunner.connect();\n await queryRunner.startTransaction();\n\n try {\n // Create task\n const task = queryRunner.manager.create(Task, {\n ...taskData,\n user,\n });\n const savedTask = await queryRunner.manager.save(task);\n\n // Create comments\n for (const commentData of comments) {\n const comment = queryRunner.manager.create(Comment, {\n ...commentData,\n task: savedTask,\n });\n await queryRunner.manager.save(comment);\n }\n\n await queryRunner.commitTransaction();\n return savedTask;\n } catch (error) {\n await queryRunner.rollbackTransaction();\n throw error;\n } finally {\n await queryRunner.release();\n }\n }\n}\n```\n\n### Step 7: Testing the Repository\n\n```typescript\n// ✅ REQUIRED: Test repository independently\n// tasks/tasks.repository.spec.ts\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { TasksRepository } from './tasks.repository';\nimport { Task } from './task.entity';\nimport { User } from '../auth/entities/user.entity';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\n\ndescribe('TasksRepository', () => {\n let repository: TasksRepository;\n\n const mockTask = {\n id: '123',\n title: 'Test Task',\n description: 'Test Description',\n status: TaskStatus.OPEN,\n user: { id: 'user-123' } as User,\n };\n\n const mockRepository = {\n createQueryBuilder: jest.fn(),\n findOne: jest.fn(),\n create: jest.fn(),\n save: jest.fn(),\n };\n\n beforeEach(async () => {\n const module: TestingModule = await Test.createTestingModule({\n providers: [\n TasksRepository,\n {\n provide: getRepositoryToken(Task),\n useValue: mockRepository,\n },\n ],\n }).compile();\n\n repository = module.get\u003cTasksRepository>(TasksRepository);\n });\n\n describe('getTasks', () => {\n it('should return filtered tasks', async () => {\n const mockQuery = {\n where: jest.fn().mockReturnThis(),\n andWhere: jest.fn().mockReturnThis(),\n getMany: jest.fn().mockResolvedValue([mockTask]),\n };\n\n mockRepository.createQueryBuilder.mockReturnValue(mockQuery);\n\n const filterDto: GetTasksFilterDto = {\n status: TaskStatus.OPEN,\n search: 'test',\n };\n\n const result = await repository.getTasks(filterDto, mockTask.user);\n\n expect(result).toEqual([mockTask]);\n expect(mockQuery.where).toHaveBeenCalled();\n expect(mockQuery.andWhere).toHaveBeenCalledTimes(2);\n });\n });\n});\n```\n\n## Installation\n\n```bash\n# For TypeORM (required)\nbun add @nestjs/typeorm typeorm\n# or\nnpm install @nestjs/typeorm typeorm\n\n# Database driver (choose one)\nbun add pg # PostgreSQL\nbun add mysql2 # MySQL\nbun add sqlite3 # SQLite\n```\n\n**Incorrect:**\n\n```typescript\n// tasks/tasks.service.ts - Database logic in service 🚨\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { Task } from './task.entity';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\nimport { User } from '../auth/entities/user.entity';\n\n@Injectable()\nexport class TasksService {\n constructor(\n @InjectRepository(Task) // ❌ Direct repository injection\n private repo: Repository\u003cTask>\n ) {}\n\n async getTasks(filterDto: GetTasksFilterDto, user: User) {\n const { status, search } = filterDto;\n\n // ❌ Database query logic in service\n const query = this.repo.createQueryBuilder('task');\n query.where({ user });\n\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n return await query.getMany();\n }\n\n async findOne(id: string, user: User) {\n // ❌ Repeated query logic\n return this.repo.findOne({ where: { id, user } });\n }\n\n async create(dto: CreateTaskDto, user: User) {\n // ❌ Mixed business and data logic\n const task = this.repo.create({ ...dto, user });\n return this.repo.save(task);\n }\n\n async findOverdue(user: User) {\n // ❌ Complex query in service - hard to test\n return this.repo\n .createQueryBuilder('task')\n .where('task.user = :user', { user })\n .andWhere('task.dueDate \u003c :now', { now: new Date() })\n .andWhere('task.status != :status', { status: TaskStatus.DONE })\n .getMany();\n }\n}\n```\n\n**Correct:**\n\n```typescript\n// tasks/tasks.repository.ts - Encapsulated database logic ✅\nimport { EntityRepository, Repository } from 'typeorm';\nimport { Task } from './task.entity';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\nimport { User } from '../auth/entities/user.entity';\nimport { TaskStatus } from './task-status.enum';\n\n@EntityRepository(Task)\nexport class TasksRepository extends Repository\u003cTask> {\n /**\n * Get tasks with optional filtering\n */\n async getTasks(filterDto: GetTasksFilterDto, user: User): Promise\u003cTask[]> {\n const { status, search } = filterDto;\n const query = this.createQueryBuilder('task');\n\n query.where({ user });\n\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n return await query.getMany();\n }\n\n /**\n * Find a single task by ID for a specific user\n */\n async findOneById(id: string, user: User): Promise\u003cTask> {\n return this.findOne({ where: { id, user } });\n }\n\n /**\n * Find overdue tasks for a user\n */\n async findOverdue(user: User): Promise\u003cTask[]> {\n return this.createQueryBuilder('task')\n .where('task.user = :user', { user })\n .andWhere('task.dueDate \u003c :now', { now: new Date() })\n .andWhere('task.status != :status', { status: TaskStatus.DONE })\n .orderBy('task.dueDate', 'ASC')\n .getMany();\n }\n\n /**\n * Get tasks with statistics\n */\n async getTasksWithStats(user: User) {\n const tasks = await this.find({ where: { user } });\n\n const stats = {\n total: tasks.length,\n open: tasks.filter((t) => t.status === TaskStatus.OPEN).length,\n inProgress: tasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,\n done: tasks.filter((t) => t.status === TaskStatus.DONE).length,\n };\n\n return { tasks, stats };\n }\n}\n\n// tasks/tasks.service.ts - Clean business logic ✅\nimport { Injectable, NotFoundException } from '@nestjs/common';\nimport { TasksRepository } from './tasks.repository';\nimport { CreateTaskDto } from './dto/create-task.dto';\nimport { UpdateTaskDto } from './dto/update-task.dto';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\nimport { User } from '../auth/entities/user.entity';\nimport { Task } from './task.entity';\n\n@Injectable()\nexport class TasksService {\n constructor(\n private tasksRepository: TasksRepository,\n ) {}\n\n /**\n * Get all tasks for a user with optional filtering\n */\n async getTasks(filterDto: GetTasksFilterDto, user: User): Promise\u003cTask[]> {\n return this.tasksRepository.getTasks(filterDto, user);\n }\n\n /**\n * Get a single task by ID\n */\n async findOne(id: string, user: User): Promise\u003cTask> {\n const task = await this.tasksRepository.findOneById(id, user);\n\n if (!task) {\n throw new NotFoundException(`Task with ID \"${id}\" not found`);\n }\n\n return task;\n }\n\n /**\n * Create a new task\n */\n async create(dto: CreateTaskDto, user: User): Promise\u003cTask> {\n const task = this.tasksRepository.create({\n ...dto,\n user,\n status: TaskStatus.OPEN,\n });\n\n return this.tasksRepository.save(task);\n }\n\n /**\n * Update an existing task\n */\n async update(id: string, dto: UpdateTaskDto, user: User): Promise\u003cTask> {\n const task = await this.findOne(id, user);\n\n // Business logic: only allow updating own tasks\n Object.assign(task, dto);\n\n return this.tasksRepository.save(task);\n }\n\n /**\n * Delete a task\n */\n async delete(id: string, user: User): Promise\u003cvoid> {\n const result = await this.tasksRepository.delete({ id, user });\n\n if (result.affected === 0) {\n throw new NotFoundException(`Task with ID \"${id}\" not found`);\n }\n }\n\n /**\n * Get overdue tasks\n */\n async getOverdue(user: User): Promise\u003cTask[]> {\n return this.tasksRepository.findOverdue(user);\n }\n}\n\n// tasks/tasks.module.ts - Proper registration ✅\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { TasksController } from './tasks.controller';\nimport { TasksService } from './tasks.service';\nimport { TasksRepository } from './tasks.repository';\nimport { Task } from './task.entity';\n\n@Module({\n imports: [TypeOrmModule.forFeature([Task])],\n controllers: [TasksController],\n providers: [TasksService, TasksRepository],\n exports: [TasksService, TasksRepository],\n})\nexport class TasksModule {}\n```\n\n## TypeORM 0.3+ Migration\n\n```typescript\n// For TypeORM >= 0.3, @EntityRepository() decorator is deprecated\n// Use dataSource-based approach:\n\n// tasks/repositories/tasks.repository.ts ✅\nimport { Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectDataSource } from '@nestjs/typeorm';\nimport { DataSource, Repository } from 'typeorm';\nimport { Task } from '../entities/task.entity';\n\n@Injectable()\nexport class TasksRepository {\n private get repo(): Repository\u003cTask> {\n return this.dataSource.getRepository(Task);\n }\n\n constructor(@InjectDataSource() private dataSource: DataSource) {}\n\n async getTasks(user: User): Promise\u003cTask[]> {\n return this.repo.find({ where: { user } });\n }\n\n async findOne(id: string, user: User): Promise\u003cTask> {\n const task = await this.repo.findOne({ where: { id, user } });\n if (!task) {\n throw new NotFoundException();\n }\n return task;\n }\n}\n\n// Or use the new TypeOrmEx decorator approach:\n// common/decorators/typeorm-ex.decorator.ts ✅\nimport { DataSource } from 'typeorm';\nimport { TYPEORM_EX_DATA_SOURCE } from './typeorm-ex.constants';\n\nexport const TypeormExRepository = \u003cT>(entity: Type\u003cany>) =>\n InjectDataSource(TYPEORM_EX_DATA_SOURCE);\n\n// tasks/repositories/tasks.repository.ts\n@Injectable()\nexport class TasksRepository {\n constructor(\n @TypeormExRepository(Task)\n private dataSource: DataSource,\n ) {\n // Use dataSource.getRepository(Task)\n }\n}\n```\n\n## Best Practices Summary\n\n| Practice | Why |\n|----------|-----|\n| Create custom repositories | Separates data access from business logic |\n| Extend TypeORM Repository | Reuse base CRUD methods |\n| Put complex queries in repository | Single responsibility, easier testing |\n| Keep services clean | Focus on business rules only |\n| Inject repository into service | Proper dependency injection |\n| Test repositories independently | Isolated data layer testing |\n\n**Sources:**\n- [TypeORM Custom Repositories](https://typeorm.io/#/custom-repository)\n- [NestJS TypeORM Documentation](https://docs.nestjs.com/techniques/database)\n- [arielweinberger/nestjs-recipe](https://github.com/arielweinberger/nestjs-recipe) - Production-ready NestJS application\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18283,"content_sha256":"97fa187f6090b09ef9388e598f20054e1ec902f13bd24bdc402da2a970b357a3"},{"filename":"rules/deployment-health-checks.md","content":"---\ntitle: Implement Health Check Endpoints\nimpact: HIGH\nsection: 11\nimpactDescription: Enables monitoring and automatic restarts\ntags: monitoring, health, production, reliability\n---\n\n## Implement Health Check Endpoints\n\nNo health checks prevent container orchestrators from detecting failures. Dedicated endpoints verify database, cache, and service connectivity. **Production apps must expose health status.**\n\n**Incorrect (no monitoring):**\n\n```typescript\n// No health endpoint - can't detect failures 🚨\n```\n\n**Correct (comprehensive health checks):**\n\n```typescript\n// health.controller.ts\n@Controller('health')\nexport class HealthController {\n constructor(\n @Inject(PrismaService) private prisma: PrismaService,\n @Inject(CacheService) private cache: CacheService,\n ) {}\n\n @Get()\n async checkHealth() {\n // Database check\n await this.prisma.$queryRaw`SELECT 1`;\n \n // Cache check\n await this.cache.get('health-check');\n \n return {\n status: 'healthy',\n timestamp: new Date().toISOString(),\n services: { database: 'ok', cache: 'ok' }\n };\n }\n}\n\n// main.ts\napp.setGlobalPrefix('api');\napp.use('/api/health', healthRouter);\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1180,"content_sha256":"f793433b7ebee2c1796a25e7de5149512e94fed05c2d2a85da33919b9771b1c3"},{"filename":"rules/error-handling-exception-filter.md","content":"---\ntitle: Enable Global Exception Filter\nimpact: CRITICAL\nsection: 4\nimpactDescription: Prevents stack trace leaks in production\ntags: security, error-handling, production, exceptions\n---\n\n## Enable Global Exception Filter\n\nDefault Express errors leak stack traces and database info to clients. Global filters catch all exceptions and return safe HTTP responses. **Hide internal errors from users.**\n\n> **Hint**: Use global exception filters to standardize error responses, log errors properly, and prevent sensitive information leaks. Different responses for development vs production environments.\n\n## Why Global Exception Filters Matter\n\n**Without global filters:**\n\n```json\n// 🚨 Production response - LEAKS sensitive info!\n{\n \"statusCode\": 500,\n \"message\": \"select * from users where id = $1 - relation \\\"users\\\" does not exist\",\n \"stack\": \"Error: relation \\\"users\\\" does not exist\\n at Connection.parseE (/app/node_modules/pg/lib/connection.js:539:11)\\n at /app/src/users/users.service.ts:42:15\\n at processTicksAndRejections (internal/process/task_queues.js:95:5)\"\n}\n```\n\n**With global filters:**\n\n```json\n// ✅ Production response - Safe!\n{\n \"statusCode\": 500,\n \"message\": \"Internal server error\",\n \"error\": \"INTERNAL_SERVER_ERROR\",\n \"timestamp\": \"2026-01-20T12:34:56.789Z\",\n \"path\": \"/api/users/123\"\n}\n```\n\n**Incorrect:**\n\n```typescript\n// main.ts\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n await app.listen(3000);\n}\n\n// Service throws raw errors\n@Injectable()\nexport class UsersService {\n async findOne(id: string) {\n const user = await this.repository.findOne(id);\n if (!user) {\n // 🚨 Throws raw Error - leaks database info\n throw new Error(`User ${id} not found in database`);\n }\n return user;\n }\n}\n```\n\n**Correct:**\n\n### Basic Implementation\n\n```typescript\n// common/filters/all-exceptions.filter.ts\nimport {\n ExceptionFilter,\n Catch,\n ArgumentsHost,\n HttpException,\n HttpStatus,\n Logger,\n} from '@nestjs/common';\n\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n private readonly logger = new Logger(AllExceptionsFilter.name);\n\n catch(exception: unknown, host: ArgumentsHost) {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse();\n const request = ctx.getRequest();\n\n const status =\n exception instanceof HttpException\n ? exception.getStatus()\n : HttpStatus.INTERNAL_SERVER_ERROR;\n\n const message =\n exception instanceof HttpException\n ? exception.message\n : 'Internal server error';\n\n // ✅ Log the full error for debugging\n this.logger.error(\n `${request.method} ${request.url}`,\n exception instanceof Error ? exception.stack : exception,\n );\n\n // ✅ Return safe response to client\n response.status(status).json({\n statusCode: status,\n message,\n timestamp: new Date().toISOString(),\n path: request.url,\n });\n }\n}\n\n// main.ts\nimport { AllExceptionsFilter } from './common/filters/all-exceptions.filter';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // Apply global filter\n app.useGlobalFilters(new AllExceptionsFilter());\n\n await app.listen(3000);\n}\n```\n\n### Environment-Aware Exception Filter\n\n```typescript\n// common/filters/all-exceptions.filter.ts\nimport { ConfigService } from '@nestjs/config';\n\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n private readonly logger = new Logger(AllExceptionsFilter.name);\n\n constructor(private configService: ConfigService) {}\n\n catch(exception: unknown, host: ArgumentsHost) {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse();\n const request = ctx.getRequest();\n\n const isDevelopment = this.configService.get('NODE_ENV') === 'development';\n\n const status =\n exception instanceof HttpException\n ? exception.getStatus()\n : HttpStatus.INTERNAL_SERVER_ERROR;\n\n // Build error response\n const errorResponse: any = {\n statusCode: status,\n timestamp: new Date().toISOString(),\n path: request.url,\n };\n\n // ✅ Development: show detailed info\n if (isDevelopment) {\n errorResponse.message =\n exception instanceof HttpException\n ? exception.message\n : exception instanceof Error\n ? exception.message\n : 'Unknown error';\n\n if (exception instanceof Error && exception.stack) {\n errorResponse.stack = exception.stack;\n }\n }\n // ✅ Production: hide details\n else {\n if (status >= 500) {\n errorResponse.message = 'Internal server error';\n } else {\n errorResponse.message =\n exception instanceof HttpException\n ? exception.message\n : 'An error occurred';\n }\n }\n\n // Log error\n this.logger.error(\n `${request.method} ${request.url} - Status: ${status}`,\n exception instanceof Error ? exception.stack : exception,\n );\n\n response.status(status).json(errorResponse);\n }\n}\n\n// main.ts - Use dependency injection\napp.useGlobalFilters(new AllExceptionsFilter(app.get(ConfigService)));\n```\n\n## Custom Business Exceptions\n\nCreate domain-specific exceptions for better error handling:\n\n```typescript\n// common/exceptions/business.exception.ts\nimport { HttpException, HttpStatus } from '@nestjs/common';\n\nexport class BusinessException extends HttpException {\n constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) {\n super(\n {\n statusCode: status,\n message,\n error: 'BUSINESS_ERROR',\n },\n status,\n );\n }\n}\n\n// common/exceptions/resource-not-found.exception.ts\nexport class ResourceNotFoundException extends BusinessException {\n constructor(resource: string, id: string) {\n super(`${resource} with id '${id}' not found`, HttpStatus.NOT_FOUND);\n }\n}\n\n// common/exceptions/invalid-state.exception.ts\nexport class InvalidStateException extends BusinessException {\n constructor(message: string) {\n super(message, HttpStatus.CONFLICT);\n }\n}\n\n// Usage in services\n@Injectable()\nexport class OrdersService {\n async cancelOrder(orderId: string) {\n const order = await this.findOne(orderId);\n\n if (order.status === OrderStatus.SHIPPED) {\n // ✅ Business logic error with clear message\n throw new InvalidStateException(\n 'Cannot cancel order: already shipped',\n );\n }\n\n order.status = OrderStatus.CANCELLED;\n return this.repository.save(order);\n }\n\n async findOne(orderId: string) {\n const order = await this.repository.findOne(orderId);\n\n if (!order) {\n // ✅ Resource not found error\n throw new ResourceNotFoundException('Order', orderId);\n }\n\n return order;\n }\n}\n```\n\n## HTTP Exception Filter\n\nHandle HTTP exceptions specifically:\n\n```typescript\n// common/filters/http-exception.filter.ts\nimport {\n ExceptionFilter,\n Catch,\n ArgumentsHost,\n HttpException,\n Logger,\n} from '@nestjs/common';\n\n@Catch(HttpException)\nexport class HttpExceptionFilter implements ExceptionFilter {\n private readonly logger = new Logger(HttpExceptionFilter.name);\n\n catch(exception: HttpException, host: ArgumentsHost) {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse();\n const request = ctx.getRequest();\n\n const status = exception.getStatus();\n const exceptionResponse = exception.getResponse();\n\n const errorResponse: any = {\n statusCode: status,\n timestamp: new Date().toISOString(),\n path: request.url,\n };\n\n // Handle different exception response types\n if (typeof exceptionResponse === 'string') {\n errorResponse.message = exceptionResponse;\n } else if (typeof exceptionResponse === 'object') {\n errorResponse.message = (exceptionResponse as any).message;\n errorResponse.error = (exceptionResponse as any).error;\n\n // Include validation errors if present\n if ((exceptionResponse as any).message instanceof Array) {\n errorResponse.errors = (exceptionResponse as any).message;\n }\n }\n\n // Log 4xx and 5xx errors\n if (status >= 400) {\n this.logger.warn(\n `${request.method} ${request.url} - ${status}: ${JSON.stringify(errorResponse.message)}`,\n );\n }\n\n response.status(status).json(errorResponse);\n }\n}\n```\n\n## Prisma Exception Filter\n\nHandle database-specific errors safely:\n\n```typescript\n// common/filters/prisma-exception.filter.ts\nimport {\n ExceptionFilter,\n Catch,\n ArgumentsHost,\n HttpStatus,\n Logger,\n} from '@nestjs/common';\nimport { Prisma } from '@prisma/client';\n\n@Catch(Prisma.PrismaClientKnownRequestError)\nexport class PrismaExceptionFilter implements ExceptionFilter {\n private readonly logger = new Logger(PrismaExceptionFilter.name);\n\n catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse();\n const request = ctx.getRequest();\n\n let status = HttpStatus.INTERNAL_SERVER_ERROR;\n let message = 'Database error occurred';\n\n // ✅ Map Prisma errors to safe messages\n switch (exception.code) {\n case 'P2002':\n status = HttpStatus.CONFLICT;\n message = 'A record with this value already exists';\n break;\n case 'P2025':\n status = HttpStatus.NOT_FOUND;\n message = 'Record not found';\n break;\n case 'P2003':\n status = HttpStatus.BAD_REQUEST;\n message = 'Related record not found';\n break;\n default:\n // Log unexpected errors with details\n this.logger.error(\n `Prisma error ${exception.code}: ${exception.message}`,\n );\n message = 'An error occurred while processing your request';\n }\n\n response.status(status).json({\n statusCode: status,\n message,\n timestamp: new Date().toISOString(),\n path: request.url,\n });\n }\n}\n```\n\n## Combining Multiple Filters\n\nUse multiple filters for different exception types:\n\n```typescript\n// main.ts\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // Order matters - more specific filters first\n app.useGlobalFilters(\n new PrismaExceptionFilter(), // Catch Prisma errors\n new HttpExceptionFilter(), // Catch HTTP exceptions\n new AllExceptionsFilter(), // Catch everything else\n );\n\n await app.listen(3000);\n}\n```\n\n## Register Filters as Global Providers\n\nBetter approach using dependency injection:\n\n```typescript\n// main.ts\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // ✅ Use APP_FILTER to enable DI\n app.get(PrismaExceptionFilter),\n app.get(HttpExceptionFilter),\n app.get(AllExceptionsFilter),\n );\n\n await app.listen(3000);\n}\n\n// Or in app.module.ts\nimport { APP_FILTER } from '@nestjs/core';\n\n@Module({\n providers: [\n {\n provide: APP_FILTER,\n useClass: AllExceptionsFilter,\n },\n {\n provide: APP_FILTER,\n useClass: HttpExceptionFilter,\n },\n ],\n})\nexport class AppModule {}\n```\n\n## Standardized Error Response Format\n\nDefine a consistent error response structure:\n\n```typescript\n// common/interfaces/error-response.interface.ts\nexport interface ErrorResponse {\n statusCode: number;\n message: string | string[];\n error?: string;\n timestamp: string;\n path: string;\n errors?: Record\u003cstring, string[]>; // For validation errors\n}\n\n// common/filters/all-exceptions.filter.ts\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n catch(exception: unknown, host: ArgumentsHost) {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse();\n const request = ctx.getRequest();\n\n const status =\n exception instanceof HttpException\n ? exception.getStatus()\n : HttpStatus.INTERNAL_SERVER_ERROR;\n\n const errorResponse: ErrorResponse = {\n statusCode: status,\n message: this.getErrorMessage(exception),\n timestamp: new Date().toISOString(),\n path: request.url,\n };\n\n response.status(status).json(errorResponse);\n }\n\n private getErrorMessage(exception: unknown): string {\n if (exception instanceof HttpException) {\n const response = exception.getResponse();\n if (typeof response === 'string') {\n return response;\n }\n return (response as any).message || 'An error occurred';\n }\n if (exception instanceof Error) {\n return exception.message;\n }\n return 'Internal server error';\n }\n}\n```\n\n## Exception Filter with Logging Service\n\nIntegrate with external logging services:\n\n```typescript\n// common/filters/all-exceptions.filter.ts\nimport { LoggerService } from '../services/logger.service';\n\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n constructor(\n private configService: ConfigService,\n private loggerService: LoggerService,\n ) {}\n\n catch(exception: unknown, host: ArgumentsHost) {\n const ctx = host.switchToHttp();\n const response = ctx.getResponse();\n const request = ctx.getResponse();\n\n const status =\n exception instanceof HttpException\n ? exception.getStatus()\n : HttpStatus.INTERNAL_SERVER_ERROR;\n\n // ✅ Log to external service (Sentry, DataDog, etc.)\n this.loggerService.logError({\n status,\n path: request.url,\n method: request.method,\n exception,\n userId: request.user?.id,\n correlationId: request.headers['x-correlation-id'],\n });\n\n const errorResponse = this.buildErrorResponse(exception, request);\n response.status(status).json(errorResponse);\n }\n}\n```\n\n## Summary: Exception Filter Best Practices\n\n| Practice | Description |\n|----------|-------------|\n| Hide stack traces in production | Never expose internal implementation details |\n| Log everything | Always log errors server-side for debugging |\n| Use environment-aware responses | Show details in dev, hide in prod |\n| Create custom exceptions | Use domain-specific exceptions for business logic |\n| Standardize error format | Consistent response structure across all endpoints |\n| Handle database errors | Map DB errors to safe HTTP responses |\n| Use correlation IDs | Track requests through distributed systems |\n\n**Sources:**\n- [Exception Filters | NestJS - Official Documentation](https://docs.nestjs.com/exception-filters)\n- [Built-in HTTP exceptions | NestJS](https://docs.nestjs.com/exception-filters#built-in-http-exceptions)\n- [Exceptions | NestJS](https://docs.nestjs.com/throw-exceptions)\n- [Error Handling Best Practices | OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14639,"content_sha256":"d494e734dac410a837613fdf828a78125ea3947acc1769320d4435cf628b6b21"},{"filename":"rules/error-handling-logger-context.md","content":"---\ntitle: Use Logger with Module Context for Debugging\nimpact: MEDIUM\nimpactDescription: Improves debugging with contextual log messages\nsection: 4\ntags: logging, error-handling, debugging, logger, context\n---\n\nUsing `console.log()` or generic loggers without context makes debugging difficult in production. NestJS's built-in Logger with module context prefixes each message with the source module, making it easy to trace where logs originate.\n\n## For AI Agents\n\nWhen implementing or reviewing logging, **always** follow these steps:\n\n### Step 1: Check for Console Logging or Contextless Loggers\n**Pattern to check:** Look for `console.log()`, `console.error()`, or Logger without context parameter.\n\n```typescript\n// ❌ WRONG - Console logging with no context\nconsole.log('User created:', user);\nconsole.error('Error:', error);\n\n// ❌ WRONG - Logger without module context\n@Injectable()\nexport class TasksService {\n private logger = new Logger(); // ❌ No context\n\n createTask(dto: CreateTaskDto) {\n this.logger.log('Creating task'); // ❌ No module prefix\n }\n}\n\n// ❌ WRONG - Generic context\n@Injectable()\nexport class TasksService {\n private logger = new Logger('Service'); // ❌ Too generic\n}\n```\n\n**If found:** Replace with contextual Logger.\n\n### Step 2: Create Logger with Class Context\n\n```typescript\n// ✅ REQUIRED: Use class name as context\nimport { Injectable, Logger } from '@nestjs/common';\n\n@Injectable()\nexport class TasksService {\n // ✅ Logger with class name as context\n private readonly logger = new Logger(TasksService.name);\n\n createTask(dto: CreateTaskDto) {\n this.logger.log('Creating task');\n // Output: [TasksService] Creating task\n }\n}\n```\n\n### Step 3: Use Appropriate Log Levels\n\n```typescript\n// ✅ REQUIRED: Use appropriate log levels\nimport { Injectable, Logger } from '@nestjs/common';\n\n@Injectable()\nexport class TasksService {\n private readonly logger = new Logger(TasksService.name);\n\n // ✅ LOG - General information\n findAll() {\n this.logger.log('Retrieving all tasks');\n return this.tasksRepository.findAll();\n }\n\n // ✅ VERBOSE - Detailed debugging (disabled by default)\n findAll(user: User, filters: GetTasksFilterDto) {\n this.logger.verbose(`User \"${user.username}\" retrieving tasks. Filters: ${JSON.stringify(filters)}`);\n return this.tasksRepository.findAll(filters, user);\n }\n\n // ✅ DEBUG - Debugging information\n async processData(data: any) {\n this.logger.debug('Processing data', { data });\n // ...\n }\n\n // ✅ WARN - Warning messages\n async updateTask(id: string, dto: UpdateTaskDto) {\n this.logger.warn(`Task ${id} is being updated`);\n // ...\n }\n\n // ✅ ERROR - Error messages\n async deleteTask(id: string) {\n this.logger.error(`Task ${id} not found`, id); // context can be any type\n throw new NotFoundException(`Task ${id} not found`);\n }\n\n // ✅ ERROR - With stack trace\n async processWithError() {\n try {\n // ...\n } catch (error) {\n this.logger.error('Processing failed', error.stack); // Pass stack trace\n throw error;\n }\n }\n}\n```\n\n### Step 4: Log Important Events\n\n```typescript\n// ✅ REQUIRED: Log lifecycle events\nimport { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';\n\n@Injectable()\nexport class TasksService implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(TasksService.name);\n\n onModuleInit() {\n this.logger.log('TasksService initialized');\n }\n\n onModuleDestroy() {\n this.logger.log('TasksService destroyed');\n }\n}\n\n// ✅ Log important business events\n@Injectable()\nexport class TasksService {\n private readonly logger = new Logger(TasksService.name);\n\n async createTask(dto: CreateTaskDto, user: User) {\n this.logger.log(`Creating task \"${dto.title}\" for user \"${user.email}\"`);\n\n const task = await this.tasksRepository.create(dto, user);\n\n this.logger.log(`Task created with ID \"${task.id}\"`);\n return task;\n }\n\n async completeTask(id: string, user: User) {\n const task = await this.tasksRepository.findOne(id, user);\n\n this.logger.verbose(`User \"${user.email}\" marking task \"${id}\" as complete`);\n\n task.status = TaskStatus.DONE;\n await this.tasksRepository.save(task);\n\n this.logger.log(`Task \"${id}\" completed by user \"${user.email}\"`);\n return task;\n }\n}\n```\n\n### Step 5: Log Request Details in Controllers\n\n```typescript\n// ✅ OPTIONAL: Log request details for debugging\nimport { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';\nimport { GetUser } from '../auth/decorators/get-user.decorator';\nimport { User } from '../users/entities/user.entity';\n\n@Controller('tasks')\nexport class TasksController {\n private readonly logger = new Logger(TasksController.name);\n\n @Get()\n findAll(@Query() filterDto: GetTasksFilterDto, @GetUser() user: User) {\n // ✅ Log user action with context\n this.logger.verbose(\n `User \"${user.username}\" retrieving tasks. Filters: ${JSON.stringify(filterDto)}`,\n );\n\n return this.tasksService.findAll(filterDto, user);\n }\n\n @Post()\n create(@Body() dto: CreateTaskDto, @GetUser() user: User) {\n // ✅ Log creation attempt\n this.logger.log(`User \"${user.username}\" creating task \"${dto.title}\"`);\n\n return this.tasksService.create(dto, user);\n }\n\n @Get(':id')\n findOne(@Param('id') id: string, @GetUser() user: User) {\n this.logger.verbose(`User \"${user.username}\" retrieving task \"${id}\"`);\n\n return this.tasksService.findOne(id, user);\n }\n}\n```\n\n### Step 6: Handle Errors with Context\n\n```typescript\n// ✅ REQUIRED: Log errors with context\nimport { Injectable, Logger, NotFoundException } from '@nestjs/common';\n\n@Injectable()\nexport class TasksService {\n private readonly logger = new Logger(TasksService.name);\n\n async findOne(id: string, user: User): Promise\u003cTask> {\n try {\n const task = await this.tasksRepository.findOne(id, user);\n\n if (!task) {\n // ✅ Log error with context before throwing\n this.logger.warn(`Task \"${id}\" not found for user \"${user.id}\"`);\n throw new NotFoundException(`Task \"${id}\" not found`);\n }\n\n return task;\n } catch (error) {\n // ✅ Log unexpected errors\n if (error instanceof NotFoundException) {\n throw error;\n }\n\n this.logger.error(\n `Failed to retrieve task \"${id}\"`,\n error.stack, // Stack trace for debugging\n );\n throw error;\n }\n }\n\n async updateTask(id: string, dto: UpdateTaskDto, user: User): Promise\u003cTask> {\n try {\n const task = await this.findOne(id, user);\n\n this.logger.log(`Updating task \"${id}\" with data: ${JSON.stringify(dto)}`);\n\n Object.assign(task, dto);\n return this.tasksRepository.save(task);\n } catch (error) {\n this.logger.error(\n `Failed to update task \"${id}\"`,\n error.stack,\n );\n throw error;\n }\n }\n}\n```\n\n### Step 7: Create Custom Logger Service (Optional)\n\n```typescript\n// ✅ OPTIONAL: Custom logger for consistent formatting\n// common/logging/logger.service.ts\nimport { Injectable, LoggerService, Scope } from '@nestjs/common';\nimport { v4 as uuidv4 } from 'uuid';\n\n@Injectable({ scope: Scope.TRANSIENT })\nexport class AppLogger implements LoggerService {\n private context: string;\n private requestId: string;\n\n setContext(context: string) {\n this.context = context;\n }\n\n setRequestId(requestId: string) {\n this.requestId = requestId;\n }\n\n log(message: any, ...optionalParams: any[]) {\n this.printMessage(message, 'LOG', optionalParams);\n }\n\n error(message: any, ...optionalParams: any[]) {\n this.printMessage(message, 'ERROR', optionalParams);\n }\n\n warn(message: any, ...optionalParams: any[]) {\n this.printMessage(message, 'WARN', optionalParams);\n }\n\n debug(message: any, ...optionalParams: any[]) {\n this.printMessage(message, 'DEBUG', optionalParams);\n }\n\n verbose(message: any, ...optionalParams: any[]) {\n this.printMessage(message, 'VERBOSE', optionalParams);\n }\n\n private printMessage(message: any, level: string, optionalParams: any[]) {\n const timestamp = new Date().toISOString();\n const requestId = this.requestId ? ` [${this.requestId}]` : '';\n const context = this.context ? ` [${this.context}]` : '';\n\n console.log(\n `[${timestamp}]${requestId}${context} [${level}] ${message}`,\n ...optionalParams,\n );\n }\n}\n\n// ✅ Use custom logger\n// tasks/tasks.service.ts\nimport { Injectable } from '@nestjs/common';\nimport { AppLogger } from '../../common/logging/logger.service';\n\n@Injectable()\nexport class TasksService {\n private readonly logger = new AppLogger();\n private readonly context = TasksService.name;\n\n constructor() {\n this.logger.setContext(this.context);\n }\n\n findAll() {\n this.logger.log('Retrieving all tasks');\n return this.tasksRepository.findAll();\n }\n}\n```\n\n## Installation\n\n```bash\n# No packages needed - Logger is built-in to NestJS\n# Just import from @nestjs/common\n```\n\n**Incorrect:**\n\n```typescript\n// tasks/tasks.service.ts - Console logging 🚨\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class TasksService {\n constructor(private tasksRepository: TasksRepository) {}\n\n // ❌ Console.log - no context, no levels\n async createTask(dto: CreateTaskDto, user: User) {\n console.log('Creating task'); // ❌ No module context\n const task = await this.tasksRepository.create(dto, user);\n console.log('Task created:', task); // ❌ No structure\n return task;\n }\n\n // ❌ Console.error for errors\n async deleteTask(id: string) {\n try {\n return await this.tasksRepository.delete(id);\n } catch (error) {\n console.error('Error deleting task:', error); // ❌ No context\n throw error;\n }\n }\n\n // ❌ No logging at all\n async updateTask(id: string, dto: UpdateTaskDto) {\n return this.tasksRepository.update(id, dto); // ❌ Silent operation\n }\n}\n\n// tasks/tasks.controller.ts - No logging 🚨\nimport { Controller, Get, Post, Body } from '@nestjs/common';\n\n@Controller('tasks')\nexport class TasksController {\n @Get()\n findAll(@Query() filterDto: GetTasksFilterDto) {\n // ❌ No logging - hard to debug in production\n return this.tasksService.findAll(filterDto);\n }\n\n @Post()\n create(@Body() dto: CreateTaskDto) {\n // ❌ No audit trail\n return this.tasksService.create(dto);\n }\n}\n```\n\n**Correct:**\n\n```typescript\n// tasks/tasks.service.ts - Contextual logging ✅\nimport { Injectable, Logger } from '@nestjs/common';\n\n@Injectable()\nexport class TasksService {\n // ✅ Logger with class context\n private readonly logger = new Logger(TasksService.name);\n\n constructor(private tasksRepository: TasksRepository) {}\n\n // ✅ Log important operations\n async createTask(dto: CreateTaskDto, user: User) {\n this.logger.log(`Creating task \"${dto.title}\" for user \"${user.email}\"`);\n\n const task = await this.tasksRepository.create(dto, user);\n\n this.logger.log(`Task created with ID \"${task.id}\"`);\n return task;\n }\n\n // ✅ Log errors with context\n async deleteTask(id: string, user: User) {\n this.logger.log(`User \"${user.email}\" deleting task \"${id}\"`);\n\n try {\n await this.tasksRepository.delete(id, user);\n this.logger.log(`Task \"${id}\" deleted`);\n } catch (error) {\n this.logger.error(\n `Failed to delete task \"${id}\"`,\n error.stack,\n );\n throw error;\n }\n }\n\n // ✅ Verbose logging for debugging\n async getTasks(filterDto: GetTasksFilterDto, user: User) {\n this.logger.verbose(\n `User \"${user.email}\" retrieving tasks with filters: ${JSON.stringify(filterDto)}`,\n );\n return this.tasksRepository.findAll(filterDto, user);\n }\n\n // ✅ Warning for edge cases\n async updateTask(id: string, dto: UpdateTaskDto, user: User) {\n const task = await this.tasksRepository.findOne(id, user);\n\n if (task.status !== dto.status) {\n this.logger.warn(`Task \"${id}\" status changing from \"${task.status}\" to \"${dto.status}\"`);\n }\n\n Object.assign(task, dto);\n const updated = await this.tasksRepository.save(task);\n\n this.logger.log(`Task \"${id}\" updated`);\n return updated;\n }\n}\n\n// tasks/tasks.controller.ts - Request logging ✅\nimport { Controller, Get, Post, Body, Query } from '@nestjs/common';\nimport { GetUser } from '../auth/decorators/get-user.decorator';\nimport { User } from '../users/entities/user.entity';\n\n@Controller('tasks')\nexport class TasksController {\n private readonly logger = new Logger(TasksController.name);\n\n constructor(private tasksService: TasksService) {}\n\n @Get()\n findAll(@Query() filterDto: GetTasksFilterDto, @GetUser() user: User) {\n // ✅ Log request with user context\n this.logger.verbose(\n `User \"${user.username}\" fetching tasks. Filters: ${JSON.stringify(filterDto)}`,\n );\n return this.tasksService.getTasks(filterDto, user);\n }\n\n @Post()\n create(@Body() dto: CreateTaskDto, @GetUser() user: User) {\n // ✅ Log creation\n this.logger.log(`User \"${user.username}\" creating task \"${dto.title}\"`);\n return this.tasksService.createTask(dto, user);\n }\n\n @Get(':id')\n findOne(@Param('id') id: string, @GetUser() user: User) {\n // ✅ Log access\n this.logger.verbose(`User \"${user.username}\" fetching task \"${id}\"`);\n return this.tasksService.findOne(id, user);\n }\n}\n\n// main.ts - Configure log level ✅\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule, {\n // ✅ Set log level based on environment\n logger: process.env.NODE_ENV === 'production'\n ? ['log', 'error', 'warn']\n : ['log', 'error', 'warn', 'debug', 'verbose'],\n });\n\n await app.listen(3000);\n}\n```\n\n## Advanced: Request ID Logging\n\n```typescript\n// ✅ Add request ID to all logs in a request\n// common/middleware/request-id.middleware.ts\nimport { Injectable, NestMiddleware, Logger } from '@nestjs/common';\nimport { Request, Response, NextFunction } from 'express';\nimport { v4 as uuidv4 } from 'uuid';\n\n@Injectable()\nexport class RequestIdMiddleware implements NestMiddleware {\n private logger = new Logger(RequestIdMiddleware.name);\n\n use(req: Request, res: Response, next: NextFunction) {\n const id = uuidv4();\n req['id'] = id;\n\n // ✅ Add request ID to response header\n res.setHeader('X-Request-ID', id);\n\n this.logger.verbose(`[${id}] ${req.method} ${req.url}`);\n\n next();\n }\n}\n\n// tasks/tasks.service.ts - Use request ID ✅\nimport { Injectable, Logger, Scope } from '@nestjs/common';\n\n@Injectable({ scope: Scope.REQUEST })\nexport class TasksService {\n private readonly logger = new Logger(TasksService.name);\n\n constructor(@Inject(REQUEST) private request: Request) {\n // ✅ Use request ID in logs\n const requestId = request['id'];\n if (requestId) {\n this.logger.setContext(`[${requestId}] ${TasksService.name}`);\n }\n }\n\n findAll() {\n this.logger.log('Retrieving all tasks');\n // Output: [abc-123] [TasksService] Retrieving all tasks\n return this.tasksRepository.findAll();\n }\n}\n```\n\n## Best Practices Summary\n\n| Practice | Why |\n|----------|-----|\n| Use `Logger(Class.name)` | Context prefix in every log message |\n| Log at appropriate levels | Control verbosity by environment |\n| Log important business events | Audit trail for debugging |\n| Log errors with stack traces | Easier debugging in production |\n| Use verbose for debugging | Disabled by default, enabled when needed |\n| Log user actions | Security audit trail |\n\n**Sources:**\n- [NestJS Logger Documentation](https://docs.nestjs.com/techniques/logger)\n- [arielweinberger/nestjs-recipe](https://github.com/arielweinberger/nestjs-recipe) - Production-ready NestJS application\n\n**See also:** `error-handling-structured-logging.md` for Winston/structured logging in production.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15782,"content_sha256":"7b9ea4d3f3354e818af0233742ef67dc1e43a1720dbc74e548d81af316d0c03b"},{"filename":"rules/error-handling-structured-logging.md","content":"---\ntitle: Implement Proper Logging Strategy\nimpact: HIGH\nsection: 4\nimpactDescription: Enables debugging, monitoring, and audit trails\ntags: logging, monitoring, debugging, production\n---\n\n## Implement Proper Logging Strategy\n\n> **Note:** For NestJS Logger with module context patterns, see `error-handling-logger-context.md`. This rule focuses on Winston and structured JSON logging for production environments.\n\nConsole.log lacks structure, levels, and persistence. NestJS Logger with Winston provides structured JSON logs for production monitoring. **Log all errors, warnings, and business events.**\n\n**Incorrect (console.log debugging):**\n\n```typescript\n// users.service.ts - No structure 🚨\nconsole.log('Creating user:', data);\ntry {\n const user = await this.prisma.user.create({ data });\n console.log('User created:', user.id);\n} catch (error) {\n console.error('User creation failed:', error);\n}\n```\n\n**Correct (structured logging):**\n\n```typescript\n// logger.service.ts\nimport { LoggerService } from '@nestjs/common';\nimport * as winston from 'winston';\n\n@Injectable()\nexport class WinstonLogger implements LoggerService {\n private logger = winston.createLogger({\n level: 'info',\n format: winston.format.combine(\n winston.format.timestamp(),\n winston.format.json()\n ),\n transports: [\n new winston.transports.Console(),\n new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),\n ],\n });\n}\n\n// users.service.ts\nimport { Logger } from '@nestjs/common';\n\n@Injectable()\nexport class UsersService {\n private readonly logger = new Logger(UsersService.name);\n\n async createUser(data: CreateUserDto) {\n this.logger.log(`Creating user for email: ${data.email}`, 'UsersService');\n \n try {\n const user = await this.prisma.user.create({ data });\n this.logger.log(`User created successfully: ${user.id}`);\n return user;\n } catch (error) {\n this.logger.error(`Failed to create user ${data.email}: ${error.message}`, error.stack);\n throw error;\n }\n }\n}\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2053,"content_sha256":"dc016570c493649156028c851c70b25eeb76ec626110b4620cd41b561dbaa433"},{"filename":"rules/interceptors-response-transform.md","content":"---\ntitle: Use Response Transformation Interceptor for Serialization\nimpact: MEDIUM\nimpactDescription: Prevents sensitive data leaks and ensures consistent response format\nsection: 14\ntags: interceptors, serialization, class-transformer, response, security\n---\n\nReturning entity objects directly from controllers can leak sensitive data (passwords, internal fields) and results in inconsistent response formats. A global transformation interceptor with `class-transformer` ensures all responses are serialized consistently and sensitive data is excluded.\n\n## For AI Agents\n\nWhen implementing or reviewing response handling, **always** follow these steps:\n\n### Step 1: Check for Direct Entity Returns\n**Pattern to check:** Look for controllers returning entity objects, repositories, or database results directly.\n\n```typescript\n// ❌ WRONG - Returns entity with password\n@Get(':id')\nfindOne(@Param('id') id: string) {\n return this.usersService.findOne(id); // ❌ Returns User with password!\n}\n\n// ❌ WRONG - Returns repository result\n@Get()\nfindAll() {\n return this.usersService.findAll(); // ❌ Returns raw entities\n}\n\n// ❌ WRONG - Manual exclusion in each method\n@Get(':id')\nfindOne(@Param('id') id: string) {\n const user = await this.usersService.findOne(id);\n // ❌ Repeated, error-prone manual exclusion\n const { password, ...result } = user;\n return result;\n}\n```\n\n**If found:** Implement TransformInterceptor.\n\n### Step 2: Create TransformInterceptor\n**File:** `src/common/interceptors/transform.interceptor.ts`\n\n```typescript\n// ✅ REQUIRED: Global response transformation\nimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';\nimport { map, Observable } from 'rxjs';\nimport { classToPlain, instanceToPlain } from 'class-transformer';\n\n@Injectable()\nexport class TransformInterceptor implements NestInterceptor {\n intercept(context: ExecutionContext, next: CallHandler): Observable\u003cany> {\n return next.handle().pipe(\n map((data) => {\n // Transform response data using class-transformer\n // This respects @Exclude() and other class-transformer decorators\n return instanceToPlain(data);\n }),\n );\n }\n}\n```\n\n### Step 3: Register Globally\n**File:** `src/main.ts`\n\n```typescript\n// ✅ REQUIRED: Register interceptor globally\nimport { TransformInterceptor } from './common/interceptors/transform.interceptor';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // ✅ Apply to all routes\n app.useGlobalInterceptors(new TransformInterceptor());\n\n await app.listen(3000);\n}\nbootstrap();\n```\n\n### Step 4: Use @Exclude() on Sensitive Fields\n\n```typescript\n// ✅ REQUIRED: Exclude sensitive fields\n// users/entities/user.entity.ts\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { Exclude } from 'class-transformer';\n\n@Entity('users')\nexport class User {\n @PrimaryGeneratedColumn('uuid')\n id: string;\n\n @Column()\n email: string;\n\n @Column()\n name: string;\n\n // ✅ Automatically excluded from all responses\n @Exclude()\n @Column()\n password: string;\n\n @Exclude()\n @Column({ name: 'internal_notes' })\n internalNotes: string;\n}\n```\n\n### Step 5: Use @Expose() for Computed Fields\n\n```typescript\n// ✅ OPTIONAL: Virtual/computed properties\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { Exclude, Expose } from 'class-transformer';\n\n@Entity('users')\nexport class User {\n @PrimaryGeneratedColumn('uuid')\n id: string;\n\n @Column()\n firstName: string;\n\n @Column()\n lastName: string;\n\n // ✅ Virtual property - not in database\n @Expose()\n get fullName(): string {\n return `${this.firstName} ${this.lastName}`;\n }\n\n // ✅ Conditional exposure\n @Expose({ groups: ['admin'] })\n @Column({ default: false })\n isAdmin: boolean;\n}\n```\n\n### Step 6: Use Groups for Role-Based Responses\n\n```typescript\n// ✅ OPTIONAL: Different response for different roles\nimport { Exclude, Expose } from 'class-transformer';\n\n@Entity('users')\nexport class User {\n @Expose({ groups: ['public', 'admin'] })\n @PrimaryGeneratedColumn('uuid')\n id: string;\n\n @Expose({ groups: ['public', 'admin'] })\n @Column()\n email: string;\n\n @Expose({ groups: ['admin'] })\n @Column()\n phoneNumber: string;\n\n @Exclude({ groups: ['public', 'admin'] })\n @Column()\n password: string;\n}\n\n// In controller/service\nimport { instanceToPlain } from 'class-transformer';\n\nreturn instanceToPlain(user, { groups: ['public'] });\n```\n\n### Step 7: Configure Serialization Options\n\n```typescript\n// ✅ OPTIONAL: Advanced interceptor with options\nimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';\nimport { map, Observable } from 'rxjs';\nimport { instanceToPlain, ClassTransformOptions } from 'class-transformer';\n\n@Injectable()\nexport class TransformInterceptor implements NestInterceptor {\n private readonly options: ClassTransformOptions;\n\n constructor(options: ClassTransformOptions = {}) {\n this.options = {\n excludePrefixes: ['_'], // Exclude properties starting with _\n ...options,\n };\n }\n\n intercept(context: ExecutionContext, next: CallHandler): Observable\u003cany> {\n return next.handle().pipe(\n map((data) => instanceToPlain(data, this.options)),\n );\n }\n}\n\n// Usage with custom options\napp.useGlobalInterceptors(\n new TransformInterceptor({\n groups: ['public'],\n version: '1.0',\n }),\n);\n```\n\n## Installation\n\n```bash\n# Install class-transformer\nbun add class-transformer\n# or\nnpm install class-transformer\n\n# For TypeORM entities (optional)\nbun add typeorm\n# or\nnpm install typeorm\n```\n\n**Incorrect:**\n\n```typescript\n// users/users.controller.ts - Leaks sensitive data 🚨\nimport { Controller, Get, Param } from '@nestjs/common';\nimport { UsersService } from './users.service';\n\n@Controller('users')\nexport class UsersController {\n constructor(private usersService: UsersService) {}\n\n // ❌ Returns entity with password field\n @Get(':id')\n async findOne(@Param('id') id: string) {\n const user = await this.usersService.findOne(id);\n // Response includes: { id, email, name, password, internalNotes }\n // Password is exposed!\n return user;\n }\n\n // ❌ Returns raw repository data\n @Get()\n async findAll() {\n // Returns all users with all fields\n return this.usersService.findAll();\n }\n\n // ❌ Manual exclusion - verbose and error-prone\n @Get('public/:id')\n async findPublic(@Param('id') id: string) {\n const user = await this.usersService.findOne(id);\n // ❌ Must remember to exclude in every method\n const { password, internalNotes, ...publicUser } = user;\n return publicUser;\n }\n}\n\n// users/entities/user.entity.ts - No exclusion decorators 🚨\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity('users')\nexport class User {\n @PrimaryGeneratedColumn('uuid')\n id: string;\n\n @Column()\n email: string;\n\n @Column()\n password: string; // ❌ Will be included in all responses\n\n @Column({ name: 'internal_notes' })\n internalNotes: string; // ❌ Leaked to clients\n}\n```\n\n**Correct:**\n\n```typescript\n// users/users.controller.ts - Clean and secure ✅\nimport { Controller, Get, Param } from '@nestjs/common';\nimport { UsersService } from './users.service';\n\n@Controller('users')\nexport class UsersController {\n constructor(private usersService: UsersService) {}\n\n // ✅ Clean - interceptor handles serialization\n @Get(':id')\n async findOne(@Param('id') id: string) {\n // Response excludes password automatically\n return this.usersService.findOne(id);\n }\n\n // ✅ Consistent across all endpoints\n @Get()\n async findAll() {\n // All users returned without sensitive fields\n return this.usersService.findAll();\n }\n\n // ✅ No manual exclusion needed\n @Get('profile')\n async getProfile(@GetUser() user: User) {\n // Password automatically excluded\n return user;\n }\n}\n\n// users/entities/user.entity.ts - Proper decorators ✅\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { Exclude, Expose } from 'class-transformer';\n\n@Entity('users')\nexport class User {\n @Expose()\n @PrimaryGeneratedColumn('uuid')\n id: string;\n\n @Expose()\n @Column()\n email: string;\n\n @Expose()\n @Column()\n name: string;\n\n // ✅ Automatically excluded from all responses\n @Exclude()\n @Column()\n password: string;\n\n // ✅ Exclude internal fields\n @Exclude()\n @Column({ name: 'internal_notes' })\n internalNotes: string;\n\n // ✅ Virtual property\n @Expose()\n get initials(): string {\n return this.firstName\n ? this.firstName[0] + this.lastName[0]\n : this.email[0];\n }\n}\n\n// common/interceptors/transform.interceptor.ts ✅\nimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';\nimport { map, Observable } from 'rxjs';\nimport { instanceToPlain } from 'class-transformer';\n\n@Injectable()\nexport class TransformInterceptor implements NestInterceptor {\n intercept(context: ExecutionContext, next: CallHandler): Observable\u003cany> {\n return next.handle().pipe(\n map((data) => {\n // Respects @Exclude, @Expose, and other decorators\n return instanceToPlain(data);\n }),\n );\n }\n}\n\n// main.ts - Global registration ✅\nimport { TransformInterceptor } from './common/interceptors/transform.interceptor';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // ✅ Apply to all routes\n app.useGlobalInterceptors(new TransformInterceptor());\n\n await app.listen(3000);\n}\n```\n\n## Advanced: Serialization Groups\n\n```typescript\n// ✅ Role-based response filtering\n// users/entities/user.entity.ts\nimport { Exclude, Expose } from 'class-transformer';\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity('users')\nexport class User {\n @Expose({ groups: ['basic', 'full', 'admin'] })\n @PrimaryGeneratedColumn('uuid')\n id: string;\n\n @Expose({ groups: ['basic', 'full', 'admin'] })\n @Column()\n email: string;\n\n @Expose({ groups: ['full', 'admin'] })\n @Column()\n phoneNumber: string;\n\n @Expose({ groups: ['admin'] })\n @Column()\n lastLoginAt: Date;\n\n @Exclude()\n @Column()\n password: string;\n}\n\n// users/users.controller.ts ✅\nimport { instanceToPlain } from 'class-transformer';\n\n@Controller('users')\nexport class UsersController {\n @Get('public/:id')\n async findPublic(@Param('id') id: string) {\n const user = await this.usersService.findOne(id);\n // Only basic fields\n return instanceToPlain(user, { groups: ['basic'] });\n }\n\n @Get(':id')\n @UseGuards(JwtAuthGuard)\n async findOne(@Param('id') id: string, @GetUser() currentUser: User) {\n const user = await this.usersService.findOne(id);\n\n // Full profile for own account\n if (user.id === currentUser.id) {\n return instanceToPlain(user, { groups: ['full'] });\n }\n\n // Basic for other users\n return instanceToPlain(user, { groups: ['basic'] });\n }\n\n @Get('admin/:id')\n @UseGuards(AdminGuard)\n async adminFindOne(@Param('id') id: string) {\n const user = await this.usersService.findOne(id);\n // All fields for admin\n return instanceToPlain(user, { groups: ['admin'] });\n }\n}\n```\n\n## Advanced: Custom Serialization\n\n```typescript\n// ✅ Custom interceptor for response wrapper\n// common/interceptors/wrap-response.interceptor.ts\nimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';\nimport { map, Observable } from 'rxjs';\nimport { instanceToPlain } from 'class-transformer';\n\nexport interface Response\u003cT> {\n success: boolean;\n data: T;\n timestamp: string;\n}\n\n@Injectable()\nexport class WrapResponseInterceptor implements NestInterceptor {\n intercept(context: ExecutionContext, next: CallHandler): Observable\u003cResponse\u003cany>> {\n return next.handle().pipe(\n map((data) => ({\n success: true,\n data: instanceToPlain(data),\n timestamp: new Date().toISOString(),\n })),\n );\n }\n}\n\n// main.ts - Combine interceptors ✅\nimport { TransformInterceptor } from './common/interceptors/transform.interceptor';\nimport { WrapResponseInterceptor } from './common/interceptors/wrap-response.interceptor';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // ✅ Order matters - wrap runs first, then transform\n app.useGlobalInterceptors(\n new WrapResponseInterceptor(),\n new TransformInterceptor(),\n );\n\n await app.listen(3000);\n}\n\n// Response format:\n// {\n// \"success\": true,\n// \"data\": { \"id\": \"123\", \"email\": \"[email protected]\" },\n// \"timestamp\": \"2026-01-20T12:00:00.000Z\"\n// }\n```\n\n## Advanced: Handle Different Response Types\n\n```typescript\n// ✅ Interceptor that handles arrays, pagination, etc.\nimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';\nimport { map, Observable } from 'rxjs';\nimport { instanceToPlain } from 'class-transformer';\n\n@Injectable()\nexport class TransformInterceptor implements NestInterceptor {\n intercept(context: ExecutionContext, next: CallHandler): Observable\u003cany> {\n return next.handle().pipe(\n map((data) => {\n // Handle Pagination\n if (data?.data !== undefined && data?.meta !== undefined) {\n return {\n ...data,\n data: instanceToPlain(data.data),\n };\n }\n\n // Handle Arrays\n if (Array.isArray(data)) {\n return data.map((item) => instanceToPlain(item));\n }\n\n // Handle single objects\n return instanceToPlain(data);\n }),\n );\n }\n}\n```\n\n## Best Practices Summary\n\n| Practice | Why |\n|----------|-----|\n| Use `@Exclude()` on sensitive fields | Automatically excluded from all responses |\n| Register TransformInterceptor globally | Consistent serialization across all routes |\n| Use `@Expose()` for virtual properties | Add computed fields to responses |\n| Use groups for role-based responses | Different data for different user types |\n| Never return raw entities | Prevents accidental data leaks |\n| Handle pagination responses | Transform paginated data correctly |\n\n**Sources:**\n- [NestJS Interceptors Documentation](https://docs.nestjs.com/interceptors)\n- [class-transformer Documentation](https://github.com/typestack/class-transformer)\n- [arielweinberger/nestjs-recipe](https://github.com/arielweinberger/nestjs-recipe) - Production-ready NestJS application\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14360,"content_sha256":"d25d12339f751a6a8159e445d3ab9b37645e09fe1f47c991dd195f6d6e6c6a5f"},{"filename":"rules/middleware-compression.md","content":"---\ntitle: Enable Compression Middleware for Responses\nimpact: HIGH\nsection: 12\nimpactDescription: Reduces bandwidth usage and improves performance\ntags: performance, compression, express, gzip\n---\n\n## Enable Compression Middleware for Responses\n\nUncompressed responses waste bandwidth and slow down page loads. Compression middleware (gzip/brotli) reduces response size by 70-90%. **Always enable compression in production.**\n\n**Incorrect (no compression):**\n\n```typescript\n// main.ts - Large responses 🚨\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n await app.listen(3000);\n}\n```\n\n**Correct (compressed responses):**\n\n```typescript\n// main.ts\nimport * as compression from 'compression';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n app.use(compression()); // Gzip/brotli compression ✅\n await app.listen(3000);\n}\n```\n\nReference: [Nestjs Compression Document](https://docs.nestjs.com/techniques/compression)","content_type":"text/markdown; charset=utf-8","language":"markdown","size":988,"content_sha256":"fa973cd5daa08135ef1434b35aa0fcfa489199014d246fcda1bf5ce8d615143a"},{"filename":"rules/middleware-rate-limiting.md","content":"---\ntitle: Implement Rate Limiting for All Routes\nimpact: CRITICAL\nsection: 12\nimpactDescription: Prevents DDoS and brute force attacks\ntags: security, performance, ddos, throttling\n---\n\n## Implement Rate Limiting for All Routes\n\nUnlimited requests per IP enable brute force attacks and DDoS. Global rate limiter throttles excessive requests. **Protect every endpoint from abuse.**\n\n**Incorrect (no protection):**\n\n```typescript\n// main.ts - Unlimited requests 🚨\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n await app.listen(3000);\n}\n```\n\n**Correct (rate limited):**\n\n```typescript\n// main.ts\nimport { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n \n app.useGlobalGuards(new ThrottlerGuard());\n \n await app.listen(3000);\n}\n\n// app.module.ts\n@Module({\n imports: [\n ThrottlerModule.forRoot([{\n ttl: 60, // 60 seconds\n limit: 10, // 10 requests per IP\n }]),\n ],\n})\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1034,"content_sha256":"6f5ef2f489a18f9ef74b97e4b0da079246ffa1c34921e53dd474706d78082efd"},{"filename":"rules/performance-redis-caching.md","content":"---\ntitle: Cache Frequently Used Data with Redis\nimpact: HIGH\nsection: 2\nimpactDescription: Dramatically reduces database load and improves response times\ntags: performance, caching, redis, scalability\n---\n\n## Cache Frequently Used Data with Redis\n\nEvery database query increases latency and costs. Redis caching stores hot data in memory for sub-millisecond access. **Cache read-heavy endpoints and expensive computations.**\n\n**Incorrect (database every request):**\n\n```typescript\n// users.service.ts - DB hit every time 🚨\nasync getUserProfile(id: string) {\n return this.prisma.user.findUnique({\n where: { id },\n include: { posts: true, followers: true }\n });\n}\n```\n\n**Correct (Redis cached):**\n\n```typescript\n// cache.service.ts\n@Injectable()\nexport class CacheService {\n constructor(private redis: Redis) {}\n \n async get\u003cT>(key: string): Promise\u003cT | null> {\n const data = await this.redis.get(key);\n return data ? JSON.parse(data) : null;\n }\n \n async set(key: string, data: any, ttl = 300) {\n await this.redis.setex(key, ttl, JSON.stringify(data));\n }\n}\n\n// users.service.ts\nasync getUserProfile(id: string) {\n const cacheKey = `user:${id}:profile`;\n let profile = await this.cacheService.get(cacheKey);\n \n if (!profile) {\n profile = await this.prisma.user.findUnique({\n where: { id },\n include: { posts: true, followers: true }\n });\n await this.cacheService.set(cacheKey, profile, 300); // 5min ✅\n }\n \n return profile;\n}\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1484,"content_sha256":"c76928ebdccca77404c562d754cf3566d22ffb79515941e82e6e15c9f1ec7d67"},{"filename":"rules/security-cors-whitelist.md","content":"---\ntitle: Enable CORS with Whitelist Origins Only\nimpact: CRITICAL\nsection: 1\nimpactDescription: Prevents unauthorized domain access\ntags: security, cors, production, express\n---\n\n## Enable CORS with Whitelist Origins Only\n\nWildcard CORS (`*`) allows malicious sites to make requests to your API. Explicit origin whitelist prevents cross-site attacks. **Never use wildcard in production.**\n\n**Incorrect (vulnerable CORS):**\n\n```typescript\n// main.ts\napp.enableCors(); // Uses '*' - dangerous! 🚨\n```\n\n**Correct (secure CORS):**\n\n```typescript\n// main.ts\napp.enableCors({\n origin: process.env.ALLOWED_ORIGINS?.split(',') || ['https://yourapp.com'],\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE'],\n allowedHeaders: ['Content-Type', 'Authorization'],\n});\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":779,"content_sha256":"e5e0ad92073b12523041a984ff6de4b7cb9789d8cfa0c3db6329e70d0817b7de"},{"filename":"rules/security-dependency-audit.md","content":"---\ntitle: Regular Dependency Security Audits\nimpact: CRITICAL\nsection: 1\nimpactDescription: Prevents exploitation of known vulnerabilities in packages\ntags: security, dependencies, npm-audit, snyk\n---\n\n## Regular Dependency Security Audits\n\nOutdated dependencies contain known vulnerabilities that attackers exploit. Automated scanning with npm audit, bun audit, and Snyk catches issues before production. **Never deploy without clean dependency audit.**\n\n**Incorrect (vulnerable deps):**\n\n```json\n// package.json - outdated packages 🚨\n{\n \"dependencies\": {\n \"lodash\": \"^4.17.20\", // CVE-2021-23337\n \"express\": \"^4.17.1\" // Multiple CVEs\n }\n}\n```\n\n**Correct (automated security):**\n\n```json\n// package.json\n{\n \"scripts\": {\n \"audit:fix\": \"npm audit fix\",\n \"audit:check\": \"npm audit --audit-level high\",\n \"audit:bun\": \"bun pm audit\",\n \"security:scan\": \"snyk test\"\n }\n}\n```\n\n```bash\n# Pre-commit / CI checks\nbun run audit:check\nnpm audit --audit-level high\nbun pm audit # Bun's audit check\nsnyk test --severity-threshold=high\n```\n\n**Workflow:**\n- `npm audit` or `bun pm audit` weekly in CI/CD\n- `npm audit fix` auto-updates patches\n- Snyk/Dependabot for vulnerability alerts\n- Pin major versions, auto-patch minors","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1250,"content_sha256":"2b127f932e1e93ae961b09f25d4393bbe3d656bbef3038efc8a78c127a53dac7"},{"filename":"rules/security-helmet-headers.md","content":"---\ntitle: Use Helmet Middleware for Security Headers\nimpact: CRITICAL\nimpactDescription: Protects against XSS, clickjacking, and other web attacks\nsection: 1\ntags: security, express, helmet, production\n---\n\n## Use Helmet Middleware for Security Headers\n\nNestJS with Express adapter exposes HTTP headers that can be exploited by attackers. Helmet automatically sets security headers like X-XSS-Protection, X-Frame-Options, and Content-Security-Policy. **Always enable it in production.**\n\n> **Hint**: Apply `helmet` as global middleware **before** other calls to `app.use()` or route definitions. Middleware order matters - routes defined before helmet will not have security headers applied.\n\n### Installation\n\n**Express (default adapter):**\n```bash\nbun add helmet\n```\n\n**Fastify adapter:**\n```bash\nbun add @fastify/helmet\n```\n\n### Usage\n\n**Incorrect (vulnerable headers):**\n\n```typescript\n// main.ts\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // Define routes before helmet - insecure!\n app.route('/api', ...);\n\n app.use(helmet());\n await app.listen(3000);\n}\n```\n\n**Correct (Express):**\n\n```typescript\n// main.ts\nimport helmet from 'helmet';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // Apply helmet FIRST, before routes and other middleware\n app.use(helmet());\n\n // Now define routes\n app.route('/api', ...);\n\n await app.listen(3000);\n}\n```\n\n**Correct (Fastify):**\n\n```typescript\n// main.ts\nimport helmet from '@fastify/helmet';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // Fastify uses register() instead of use()\n await app.register(helmet);\n\n await app.listen(3000);\n}\n```\n\n### Apollo/GraphQL CSP Configuration\n\nWhen using `@apollo/server` (4.x) with Apollo Sandbox, default CSP may cause issues. Configure as follows:\n\n**Express with Apollo:**\n```typescript\napp.use(helmet({\n crossOriginEmbedderPolicy: false,\n contentSecurityPolicy: {\n directives: {\n imgSrc: [`'self'`, 'data:', 'apollo-server-landing-page.cdn.apollographql.com'],\n scriptSrc: [`'self'`, `https: 'unsafe-inline'`],\n manifestSrc: [`'self'`, 'apollo-server-landing-page.cdn.apollographql.com'],\n frameSrc: [`'self'`, 'sandbox.embed.apollographql.com'],\n },\n },\n}));\n```\n\n**Fastify with Apollo:**\n```typescript\nawait app.register(helmet, {\n contentSecurityPolicy: {\n directives: {\n defaultSrc: [`'self'`, 'unpkg.com'],\n styleSrc: [\n `'self'`,\n `'unsafe-inline'`,\n 'cdn.jsdelivr.net',\n 'fonts.googleapis.com',\n 'unpkg.com',\n ],\n fontSrc: [`'self'`, 'fonts.gstatic.com', 'data:'],\n imgSrc: [`'self'`, 'data:', 'cdn.jsdelivr.net'],\n scriptSrc: [\n `'self'`,\n `https: 'unsafe-inline'`,\n 'cdn.jsdelivr.net',\n `'unsafe-eval'`,\n ],\n },\n },\n});\n\n// Or disable CSP entirely if not needed:\nawait app.register(helmet, {\n contentSecurityPolicy: false,\n});\n```\n\nReference: [Helmet NestJS Documentation](https://docs.nestjs.com/security/helmet)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3071,"content_sha256":"bc85272b408bcd9cd5eef8c426b830f91161748a196b3a52f9898f36fd5ed75c"},{"filename":"rules/testing-unit-tests.md","content":"---\ntitle: Write Comprehensive Unit Tests\nsection: 10\nimpact: MEDIUM\nimpactDescription: Catches bugs early and enables safe refactoring\ntags: testing, tdd, jest, quality\n---\n\n## Write Comprehensive Unit Tests\n\nUntested code leads to regressions and production bugs. NestJS + Jest provides full testing support for controllers, services, and repositories. **Aim for 80%+ coverage on business logic.**\n\n**Incorrect (no tests):**\n\n```typescript\n// users.service.ts - No tests 🚨\n@Injectable()\nexport class UsersService {\n async createUser(data: CreateUserDto) {\n return this.prisma.user.create({ data });\n }\n}\n```\n\n**Correct (full test suite):**\n\n```typescript\n// users.service.spec.ts\ndescribe('UsersService', () => {\n let service: UsersService;\n let prisma: PrismaService;\n\n beforeEach(async () => {\n const module = await Test.createTestingModule({\n providers: [UsersService, { provide: PrismaService, useValue: mockPrisma }],\n }).compile();\n service = module.get(UsersService);\n });\n\n it('should create user successfully', async () => {\n const dto: CreateUserDto = { email: '[email protected]', name: 'Test' };\n const mockUser = { id: '1', ...dto };\n\n jest.spyOn(prisma.user, 'create').mockResolvedValue(mockUser);\n\n const result = await service.createUser(dto);\n expect(result).toEqual(mockUser);\n expect(prisma.user.create).toHaveBeenCalledWith(expect.any(Object));\n });\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1425,"content_sha256":"fe5a3c16777134f582412b399aabdecc1bf7e23d61c7266f3e143dd469676820"},{"filename":"rules/validation-custom-pipes.md","content":"---\ntitle: Create Custom Pipes for Query Parameter Transformation\nimpact: MEDIUM\nsection: 5\nimpactDescription: Ensures type safety and reduces boilerplate\ntags: validation, pipes, transformation, type-safety\n---\n\nQuery parameters are always strings by default. Custom pipes automatically transform and validate these values before they reach your controller, providing type safety and reducing boilerplate code. **Never manually parse query parameters in controllers.**\n\n> **Hint**: NestJS pipes are executed before controllers. Use them to transform `?page=1` into `number: 1`, `?active=true` into `boolean: true`, and trim whitespace from strings automatically.\n\n## For AI Agents\n\nWhen implementing or reviewing query parameter handling, **always** follow these steps:\n\n### Step 1: Check for Manual Type Conversion\n**Pattern to check:** Look for `parseInt()`, `Number()`, `Boolean()`, or string operations in controllers.\n\n```typescript\n// ❌ WRONG - Manual conversion in controller\n@Get('users')\nasync getUsers(@Query('page') page: string) {\n const pageNum = parseInt(page, 10); // Manual conversion!\n const limit = 10;\n return this.usersService.findAll({ page: pageNum, limit });\n}\n\n// ✅ CORRECT - Custom pipe handles conversion\n@Get('users')\nasync getUsers(\n @Query('page', ParseIntPipe) page: number,\n @Query('limit', new ParseIntPipe({ optional: true })) limit?: number,\n) {\n return this.usersService.findAll({ page, limit: limit || 10 });\n}\n```\n\n**If found:** Create custom pipes to handle the conversion.\n\n### Step 2: Create Basic Transformation Pipes\n**File:** `src/common/pipes/parse-int.pipe.ts`\n\n```typescript\n// ✅ REQUIRED: ParseIntPipe with optional support\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\n\n@Injectable()\nexport class ParseIntPipe implements PipeTransform\u003cstring, number> {\n constructor(private options?: { optional?: boolean }) {}\n\n transform(value: string, metadata: ArgumentMetadata): number {\n if (this.options?.optional && !value) {\n return undefined as any;\n }\n\n const isNumeric = ['string', 'number'].includes(typeof value) &&\n !isNaN(parseFloat(value)) &&\n isFinite(parseFloat(value));\n\n if (!isNumeric) {\n throw new BadRequestException(`Validation failed: \"${value}\" is not a number`);\n }\n\n return parseFloat(value);\n }\n}\n```\n\n**File:** `src/common/pipes/trim.pipe.ts`\n\n```typescript\n// ✅ REQUIRED: TrimPipe for strings\nimport { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';\n\n@Injectable()\nexport class TrimPipe implements PipeTransform {\n transform(value: any, metadata: ArgumentMetadata) {\n if (typeof value === 'string') {\n return value.trim();\n }\n return value;\n }\n}\n\n// For arrays and objects\n@Injectable()\nexport class DeepTrimPipe implements PipeTransform {\n transform(value: any, metadata: ArgumentMetadata) {\n if (typeof value === 'string') {\n return value.trim();\n }\n if (Array.isArray(value)) {\n return value.map(item => this.transform(item, metadata));\n }\n if (value && typeof value === 'object') {\n return Object.keys(value).reduce((acc, key) => {\n acc[key] = this.transform(value[key], metadata);\n return acc;\n }, {} as any);\n }\n return value;\n }\n}\n```\n\n**File:** `src/common/pipes/parse-boolean.pipe.ts`\n\n```typescript\n// ✅ REQUIRED: ParseBooleanPipe\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\n\n@Injectable()\nexport class ParseBooleanPipe implements PipeTransform\u003cstring, boolean> {\n transform(value: string, metadata: ArgumentMetadata): boolean {\n if (value === 'true' || value === '1') {\n return true;\n }\n if (value === 'false' || value === '0') {\n return false;\n }\n throw new BadRequestException(\n `Validation failed: \"${value}\" is not a boolean. Use \"true\" or \"false\"`\n );\n }\n}\n```\n\n### Step 3: Register Pipes Globally or Use Per-Parameter\n\n```typescript\n// ✅ OPTIONAL: Global trim pipe in main.ts\nimport { ValidationPipe } from '@nestjs/common';\nimport { TrimPipe } from './common/pipes/trim.pipe';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // Apply trim globally to all body/query/param values\n app.useGlobalPipes(new ValidationPipe({\n transform: true,\n whitelist: true,\n transformOptions: {\n enableImplicitConversion: true,\n },\n }));\n\n await app.listen(3000);\n}\n```\n\n### Step 4: Use Pipes in Controllers\n\n```typescript\n// ✅ CORRECT: Controller with pipes\n@Controller('users')\nexport class UsersController {\n @Get()\n findAll(\n @Query('page', new ParseIntPipe({ optional: true })) page?: number,\n @Query('limit', new ParseIntPipe({ optional: true })) limit?: number,\n @Query('active', new ParseBooleanPipe({ optional: true })) active?: boolean,\n @Query('search', TrimPipe) search?: string,\n ) {\n return this.usersService.findAll({\n page: page || 1,\n limit: limit || 10,\n active,\n search,\n });\n }\n}\n```\n\n## Installation\n\n```bash\n# Pipes are built-in to NestJS - no extra packages needed\nbun add @nestjs/common\n```\n\n## Quick Reference Checklist\n\nUse this checklist when reviewing or creating query parameter handling:\n\n- [ ] No manual `parseInt()` or `Number()` in controllers\n- [ ] No manual `toLowerCase()` or `trim()` in controllers\n- [ ] All numeric query params use `ParseIntPipe` or `ParseFloatPipe`\n- [ ] All boolean query params use `ParseBooleanPipe`\n- [ ] All string params use `TrimPipe` if whitespace matters\n- [ ] Optional params use `optional: true` pipe option\n- [ ] Error messages from pipes are user-friendly\n\n**Incorrect:**\n\n```typescript\n// users.controller.ts - Manual type conversion 🚨\n@Controller('users')\nexport class UsersController {\n @Get()\n async findAll(@Query() query: any) {\n // ❌ Manual parsing - error-prone\n const page = parseInt(query.page as string, 10) || 1;\n const limit = parseInt(query.limit as string, 10) || 10;\n\n // ❌ Manual boolean conversion\n const active = query.active === 'true' || query.active === '1';\n\n // ❌ Manual trimming\n const search = query.search ? (query.search as string).trim() : undefined;\n\n // ❌ No validation - NaN possible\n if (isNaN(page) || isNaN(limit)) {\n throw new BadRequestException('Invalid page or limit');\n }\n\n return this.usersService.findAll({ page, limit, active, search });\n }\n\n @Get(':id')\n async findOne(@Param('id') id: string) {\n // ❌ Manual integer conversion\n const userId = parseInt(id, 10);\n if (isNaN(userId)) {\n throw new BadRequestException('Invalid user ID');\n }\n return this.usersService.findOne(userId);\n }\n}\n```\n\n**Correct:**\n\n```typescript\n// common/pipes/parse-int.pipe.ts ✅\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\n\n@Injectable()\nexport class ParseIntPipe implements PipeTransform\u003cstring, number> {\n constructor(private options?: { optional?: boolean; minValue?: number; maxValue?: number }) {}\n\n transform(value: string, metadata: ArgumentMetadata): number {\n // Handle optional empty values\n if (this.options?.optional && (value === undefined || value === null || value === '')) {\n return undefined as any;\n }\n\n const parsed = parseInt(value, 10);\n\n if (isNaN(parsed)) {\n throw new BadRequestException(\n `Validation failed: \"${metadata.data}\" must be a valid integer`\n );\n }\n\n // Range validation\n if (this.options?.minValue !== undefined && parsed \u003c this.options.minValue) {\n throw new BadRequestException(\n `Validation failed: \"${metadata.data}\" must be at least ${this.options.minValue}`\n );\n }\n\n if (this.options?.maxValue !== undefined && parsed > this.options.maxValue) {\n throw new BadRequestException(\n `Validation failed: \"${metadata.data}\" must be at most ${this.options.maxValue}`\n );\n }\n\n return parsed;\n }\n}\n\n// common/pipes/parse-float.pipe.ts ✅\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\n\n@Injectable()\nexport class ParseFloatPipe implements PipeTransform\u003cstring, number> {\n constructor(private options?: { optional?: boolean; min?: number; max?: number }) {}\n\n transform(value: string, metadata: ArgumentMetadata): number {\n if (this.options?.optional && !value) {\n return undefined as any;\n }\n\n const parsed = parseFloat(value);\n\n if (isNaN(parsed)) {\n throw new BadRequestException(`Validation failed: \"${metadata.data}\" must be a valid number`);\n }\n\n if (this.options?.min !== undefined && parsed \u003c this.options.min) {\n throw new BadRequestException(`Validation failed: \"${metadata.data}\" must be at least ${this.options.min}`);\n }\n\n if (this.options?.max !== undefined && parsed > this.options.max) {\n throw new BadRequestException(`Validation failed: \"${metadata.data}\" must be at most ${this.options.max}`);\n }\n\n return parsed;\n }\n}\n\n// common/pipes/parse-boolean.pipe.ts ✅\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\n\n@Injectable()\nexport class ParseBooleanPipe implements PipeTransform\u003cstring, boolean> {\n constructor(private options?: { optional?: boolean }) {}\n\n transform(value: string, metadata: ArgumentMetadata): boolean {\n if (this.options?.optional && !value) {\n return undefined as any;\n }\n\n const normalized = value.toLowerCase().trim();\n\n if (['true', '1', 'yes'].includes(normalized)) {\n return true;\n }\n\n if (['false', '0', 'no'].includes(normalized)) {\n return false;\n }\n\n throw new BadRequestException(\n `Validation failed: \"${metadata.data}\" must be a boolean (true/false, yes/no, 1/0)`\n );\n }\n}\n\n// common/pipes/trim.pipe.ts ✅\nimport { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';\n\n@Injectable()\nexport class TrimPipe implements PipeTransform\u003cstring, string> {\n transform(value: string, metadata: ArgumentMetadata): string {\n if (typeof value !== 'string') {\n return value;\n }\n return value.trim();\n }\n}\n\n// common/pipes/default-value.pipe.ts ✅\nimport { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';\n\n@Injectable()\nexport class DefaultValuePipe\u003cT = any> implements PipeTransform\u003cT, T> {\n constructor(private readonly defaultValue: T) {}\n\n transform(value: T, metadata: ArgumentMetadata): T {\n return value === undefined || value === null || value === ''\n ? this.defaultValue\n : value;\n }\n}\n\n// users.controller.ts - Clean with pipes ✅\n@Controller('users')\nexport class UsersController {\n constructor(private usersService: UsersService) {}\n\n @Get()\n findAll(\n @Query('page', new ParseIntPipe({ optional: true, minValue: 1 })) page?: number,\n @Query('limit', new ParseIntPipe({ optional: true, minValue: 1, maxValue: 100 })) limit?: number,\n @Query('active', new ParseBooleanPipe({ optional: true })) active?: boolean,\n @Query('search', TrimPipe) search?: string,\n @Query('sortBy', new DefaultValuePipe('createdAt')) sortBy?: string,\n @Query('order', new DefaultValuePipe('DESC')) order?: 'ASC' | 'DESC',\n ) {\n return this.usersService.findAll({\n page: page || 1,\n limit: limit || 10,\n active,\n search,\n sortBy,\n order,\n });\n }\n\n @Get(':id')\n findOne(\n @Param('id', ParseIntPipe) id: number,\n ) {\n return this.usersService.findOne(id);\n }\n}\n```\n\n## Pipe Composition and Chaining\n\n### Combining Multiple Pipes\n\n```typescript\n// Apply multiple pipes to a single parameter\n@Get('products')\nasync getProducts(\n @Query('price', ParseFloatPipe, new DefaultValuePipe(0)) minPrice: number,\n @Query('rating', ParseIntPipe, new DefaultValuePipe(0)) minRating: number,\n) {\n return this.productsService.findFiltered({ minPrice, minRating });\n}\n```\n\n### Custom Validation Pipe\n\n```typescript\n// common/pipes/enum.pipe.ts ✅\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\n\n@Injectable()\nexport class EnumPipe\u003cT = any> implements PipeTransform\u003cstring, T> {\n constructor(private readonly enumObject: Record\u003cstring, T>) {}\n\n transform(value: string, metadata: ArgumentMetadata): T {\n const enumValues = Object.values(this.enumObject);\n\n if (enumValues.includes(value as T)) {\n return value as T;\n }\n\n throw new BadRequestException(\n `Validation failed: \"${metadata.data}\" must be one of: ${enumValues.join(', ')}`\n );\n }\n}\n\n// Usage\nenum SortOrder {\n ASC = 'ASC',\n DESC = 'DESC',\n}\n\nenum SortField {\n NAME = 'name',\n CREATED_AT = 'createdAt',\n UPDATED_AT = 'updatedAt',\n}\n\n@Get('users')\nasync getUsers(\n @Query('order', new EnumPipe(SortOrder)) order: SortOrder,\n @Query('sortBy', new EnumPipe(SortField)) sortBy: SortField,\n) {\n return this.usersService.findAll({ order, sortBy });\n}\n```\n\n## Error Handling in Pipes\n\n### Custom Error Messages\n\n```typescript\n// common/pipes/parse-uuid.pipe.ts ✅\nimport { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';\n\n@Injectable()\nexport class ParseUuidPipe implements PipeTransform\u003cstring, string> {\n constructor(private options?: { optional?: boolean; version?: 4 | 7 }) {}\n\n transform(value: string, metadata: ArgumentMetadata): string {\n if (this.options?.optional && !value) {\n return undefined as any;\n }\n\n const uuidRegex = this.options?.version === 7\n ? /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n : /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n if (!uuidRegex.test(value)) {\n throw new BadRequestException(\n `Validation failed: \"${metadata.data}\" must be a valid UUID${this.options?.version ? ` v${this.options.version}` : ''}`\n );\n }\n\n return value;\n }\n}\n```\n\n### Formatting Error Response\n\n```typescript\n// common/exceptions/validation.exception.ts ✅\nimport { BadRequestException } from '@nestjs/common';\n\nexport class ValidationException extends BadRequestException {\n constructor(field: string, message: string, value?: any) {\n super({\n statusCode: 400,\n message: 'Validation failed',\n errors: [\n {\n field,\n message,\n value,\n },\n ],\n });\n }\n}\n\n// Use in pipe\n@Injectable()\nexport class ParseIntPipe implements PipeTransform\u003cstring, number> {\n transform(value: string, metadata: ArgumentMetadata): number {\n const parsed = parseInt(value, 10);\n\n if (isNaN(parsed)) {\n throw new ValidationException(\n metadata.data,\n 'Must be a valid integer',\n value\n );\n }\n\n return parsed;\n }\n}\n```\n\n## Global Pipe Registration\n\n### Registering Pipes Application-Wide\n\n```typescript\n// main.ts ✅\nimport { ValidationPipe, Logger } from '@nestjs/common';\nimport { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\nimport { TrimPipe } from './common/pipes/trim.pipe';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n // Global validation pipe with all best practices\n app.useGlobalPipes(new ValidationPipe({\n whitelist: true, // Strip properties not in DTO\n forbidNonWhitelisted: true, // Throw error if extra properties\n transform: true, // Convert plain objects to DTO instances\n transformOptions: {\n enableImplicitConversion: true, // Auto-convert primitives\n },\n disableErrorMessages: false, // Show detailed errors in development\n exceptionFactory: (errors) => {\n // Custom error format\n return new BadRequestException({\n statusCode: 400,\n message: 'Validation failed',\n errors: errors.map(error => ({\n field: error.property,\n constraints: error.constraints,\n value: error.value,\n })),\n });\n },\n }));\n\n await app.listen(3000);\n}\nbootstrap();\n```\n\n### Module-Level Pipe Registration\n\n```typescript\n// users.module.ts ✅\nimport { Module } from '@nestjs/common';\nimport { USERS_SERVICES } from './users.providers';\nimport { ParseIntPipe } from '../common/pipes/parse-int.pipe';\n\n@Module({\n providers: [\n ...USERS_SERVICES,\n ParseIntPipe, // Register pipe for use in this module\n ],\n exports: [ParseIntPipe], // Export if other modules need it\n})\nexport class UsersModule {}\n```\n\n## Testing Custom Pipes\n\n```typescript\n// common/pipes/parse-int.pipe.spec.ts ✅\nimport { ParseIntPipe } from './parse-int.pipe';\nimport { BadRequestException } from '@nestjs/common';\n\ndescribe('ParseIntPipe', () => {\n let pipe: ParseIntPipe;\n\n beforeEach(() => {\n pipe = new ParseIntPipe();\n });\n\n it('should parse valid integer string', () => {\n const result = pipe.transform('42', { type: 'query', data: 'page' } as any);\n expect(result).toBe(42);\n });\n\n it('should throw for invalid string', () => {\n expect(() => pipe.transform('abc', { type: 'query', data: 'page' } as any))\n .toThrow(BadRequestException);\n });\n\n it('should return undefined for optional empty value', () => {\n const optionalPipe = new ParseIntPipe({ optional: true });\n const result = optionalPipe.transform('', { type: 'query', data: 'page' } as any);\n expect(result).toBeUndefined();\n });\n\n it('should enforce min value', () => {\n const pipeWithMin = new ParseIntPipe({ minValue: 1 });\n expect(() => pipeWithMin.transform('0', { type: 'query', data: 'page' } as any))\n .toThrow(BadRequestException);\n });\n\n it('should enforce max value', () => {\n const pipeWithMax = new ParseIntPipe({ maxValue: 100 });\n expect(() => pipeWithMax.transform('101', { type: 'query', data: 'page' } as any))\n .toThrow(BadRequestException);\n });\n});\n```\n\n## Best Practices Summary\n\n| Practice | Why |\n|----------|-----|\n| Use pipes for conversion | Consistent transformation, reusable |\n| Make pipes reusable | Add optional/min/max options |\n| Return user-friendly errors | Help API consumers fix their requests |\n| Use `DefaultValuePipe` | Set sensible defaults |\n| Combine pipes | Chain multiple transformations |\n| Test pipes independently | Ensure reliability |\n\n**Sources:**\n- [Pipes | NestJS - Official Documentation](https://docs.nestjs.com/pipes)\n- [Custom Pipes | NestJS Documentation](https://docs.nestjs.com/techniques/validation)\n- [Query String Params | MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18532,"content_sha256":"71dd99a6be2a625277eaac3372fd98b3c2881dc1af6f8139bbdcb4144911406e"},{"filename":"rules/validation-dto-validation.md","content":"---\ntitle: Validate All Inputs with DTOs and ValidationPipe\nimpact: CRITICAL\nimpactDescription: Prevents invalid data and injection attacks\nsection: 5\ntags: validation, security, dto, class-validator\n---\n\n## Validate All Inputs with DTOs and ValidationPipe\n\nUnvalidated inputs lead to runtime errors, SQL injection, and security vulnerabilities. Global ValidationPipe ensures every DTO is validated before reaching controllers. **Never trust client data.**\n\n**Incorrect:**\n\n```typescript\n// main.ts - Missing ValidationPipe\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n await app.listen(3000); // No global pipe!\n}\n\n// users.controller.ts - No validation\n@Post()\ncreate(@Body() createUserDto: any) {\n // Any data can be passed - vulnerable!\n return this.usersService.create(createUserDto);\n}\n\n// dto/create-user.dto.ts - No decorators\nexport class CreateUserDto {\n email: string; // No validation\n password: string; // No validation\n age: number; // No validation\n}\n```\n\n**Correct:**\n\n```typescript\n// main.ts - Global ValidationPipe\nimport { ValidationPipe } from '@nestjs/common';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n app.useGlobalPipes(new ValidationPipe({\n whitelist: true, // Strip properties not in DTO\n forbidNonWhitelisted: true, // Throw error if extra properties\n transform: true, // Convert plain objects to DTO instances\n transformOptions: {\n enableImplicitConversion: true,\n },\n disableErrorMessages: false, // Show detailed validation errors\n }));\n\n await app.listen(3000);\n}\n\n// dto/create-user.dto.ts - Full validation\nimport { IsEmail, IsString, MinLength, IsOptional, IsInt, Matches } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class CreateUserDto {\n @IsEmail({}, { message: 'Invalid email format' })\n email: string;\n\n @IsString()\n @MinLength(8, { message: 'Password must be at least 8 characters' })\n @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/, {\n message: 'Password must contain uppercase, lowercase, and number'\n })\n password: string;\n\n @IsOptional()\n @IsString()\n @MinLength(2)\n name?: string;\n\n @IsOptional()\n @IsInt()\n @Min(18)\n @Max(120)\n age?: number;\n}\n\n// users.controller.ts - Type-safe\n@Post()\ncreate(@Body() createUserDto: CreateUserDto) {\n // DTO is automatically validated before reaching here\n return this.usersService.create(createUserDto);\n}\n```\n\n## ValidationPipe Options Reference\n\n| Option | What It Does | Always Use? |\n|--------|--------------|------------|\n| `whitelist: true` | Removes properties not in DTO | ✅ Yes |\n| `forbidNonWhitelisted: true` | Throws error if extra properties sent | ✅ Yes (security) |\n| `transform: true` | Converts plain objects to DTO instances | ✅ Yes (type safety) |\n| `disableErrorMessages: false` | Shows detailed validation errors | ✅ Yes (debugging) |\n| `transformOptions.enableImplicitConversion: true` | Auto-converts strings to types | ✅ Yes (convenience) |\n\n## Common Validation Decorators\n\n```typescript\nimport {\n // String validation\n IsEmail, IsString, IsUrl, IsUUID, IsPhoneNumber,\n MinLength, MaxLength, Matches,\n\n // Number validation\n IsInt, IsFloat, IsPositive, IsNegative,\n Min, Max, IsNumber,\n\n // Type validation\n IsBoolean, IsDate, IsArray, IsObject,\n\n // Conditional\n IsOptional, IsEmpty,\n\n // Enum & Sets\n IsEnum, IsIn, IsNotIn,\n\n // Array validation\n ArrayNotEmpty, ArrayMinSize, ArrayMaxSize,\n\n // Nested validation\n ValidateNested,\n} from 'class-validator';\n\nimport { Type, Expose } from 'class-transformer';\n```\n\n> **Note:** For query parameter validation with filter DTOs, see `validation-filter-dtos.md`.\n\n## Custom Validators\n\nFor business-specific validation rules:\n\n```typescript\n// decorators/is-strong-password.decorator.ts\nimport {\n registerDecorator,\n ValidationOptions,\n ValidatorConstraint,\n ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'isStrongPassword', async: false })\nexport class IsStrongPasswordConstraint implements ValidatorConstraintInterface {\n validate(password: string) {\n // At least 8 chars, 1 uppercase, 1 lowercase, 1 number, 1 special char\n const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$/;\n return strongPasswordRegex.test(password);\n }\n\n defaultMessage() {\n return 'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character';\n }\n}\n\nexport function IsStrongPassword(validationOptions?: ValidationOptions) {\n return function (object: Object, propertyName: string) {\n registerDecorator({\n target: object.constructor,\n propertyName: propertyName,\n options: validationOptions,\n constraints: [],\n validator: IsStrongPasswordConstraint,\n });\n };\n}\n\n// dto/create-user.dto.ts\nexport class CreateUserDto {\n @IsStrongPassword()\n password: string;\n}\n```\n\n## Nested DTOs\n\n```typescript\n// dto/create-order.dto.ts\nimport { ValidateNested, IsArray, ArrayMinSize } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class OrderItemDto {\n @IsString()\n productId: string;\n\n @IsInt()\n @Min(1)\n quantity: number;\n}\n\nexport class CreateOrderDto {\n @ValidateNested()\n @Type(() => AddressDto)\n shippingAddress: AddressDto;\n\n @IsArray()\n @ArrayMinSize(1)\n @ValidateNested({ each: true })\n @Type(() => OrderItemDto)\n items: OrderItemDto[];\n}\n```\n\n## Expected Validation Error Response\n\n```json\n{\n \"statusCode\": 400,\n \"message\": [\n \"email must be an email\",\n \"password must be longer than or equal to 8 characters\",\n \"password must contain uppercase, lowercase, and number\",\n \"age must not be less than 18\"\n ],\n \"error\": \"Bad Request\"\n}\n```\n\n**Sources:**\n- [NestJS Validation Documentation](https://docs.nestjs.com/techniques/validation)\n- [class-validator Documentation](https://github.com/typestack/class-validator)\n- [class-transformer Documentation](https://github.com/typestack/class-transformer)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6088,"content_sha256":"38860f236f9c1d02ca0e9519358255962f2fbecc567a41210738cff99c3e1843"},{"filename":"rules/validation-filter-dtos.md","content":"---\ntitle: Use Filter DTOs for Query Parameter Validation\nimpact: HIGH\nimpactDescription: Validates and type-checks query parameters\nsection: 5\ntags: validation, query, dto, filter, search, pagination\n---\n\nExtracting query parameters individually (`@Query('status') status: string`) results in no validation, no type safety, and verbose code. Filter DTOs validate all query parameters consistently and provide automatic type transformation.\n\n## For AI Agents\n\nWhen implementing or reviewing query parameter handling, **always** follow these steps:\n\n### Step 1: Check for Individual Query Parameters\n**Pattern to check:** Look for `@Query()` decorators with parameter names, manual string parsing, or optional chaining.\n\n```typescript\n// ❌ WRONG - Individual parameters with no validation\n@Get()\nfindAll(\n @Query('status') status: string, // ❌ No validation\n @Query('search') search: string, // ❌ No validation\n @Query('page') page: string, // ❌ String instead of number\n @Query('limit') limit: string, // ❌ String instead of number\n) {\n // ❌ Manual type conversion\n const pageNum = parseInt(page) || 1;\n const limitNum = parseInt(limit) || 10;\n // ...\n}\n\n// ❌ WRONG - Manual parsing with no validation\n@Get()\nfindAll(@Query() query: any) {\n const status = query.status as string; // ❌ Type assertion\n const page = parseInt(query.page) || 1; // ❌ Manual parse\n const limit = parseInt(query.limit) || 10; // ❌ Manual parse\n // ...\n}\n```\n\n**If found:** Replace with filter DTO.\n\n### Step 2: Create Filter DTO\n**File:** `tasks/dto/get-tasks-filter.dto.ts`\n\n```typescript\n// ✅ REQUIRED: Filter DTO with validation\nimport { IsOptional, IsEnum, IsString, IsInt, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { TaskStatus } from '../task-status.enum';\n\nexport class GetTasksFilterDto {\n // ✅ Optional enum validation\n @IsOptional()\n @IsEnum(TaskStatus)\n status?: TaskStatus;\n\n // ✅ Optional string with validation\n @IsOptional()\n @IsString()\n search?: string;\n\n // ✅ Auto-transform string to number\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n page?: number = 1;\n\n // ✅ Auto-transform string to number\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n @Max(100)\n limit?: number = 10;\n}\n```\n\n### Step 3: Use Filter DTO in Controller\n\n```typescript\n// ✅ REQUIRED: Use DTO in controller\n// tasks/tasks.controller.ts\nimport { Get, Query, Controller } from '@nestjs/common';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\n\n@Controller('tasks')\nexport class TasksController {\n @Get()\n findAll(@Query() filterDto: GetTasksFilterDto) {\n // ✅ filterDto is validated and typed\n // filterDto.status is TaskStatus | undefined\n // filterDto.page is number (not string!)\n // filterDto.limit is number (not string!)\n return this.tasksService.getTasks(filterDto);\n }\n}\n```\n\n### Step 4: Create Reusable Pagination DTO\n\n```typescript\n// ✅ OPTIONAL: Base pagination DTO\n// common/dto/pagination.dto.ts\nimport { IsOptional, IsInt, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class PaginationDto {\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n page?: number = 1;\n\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n @Max(100)\n limit?: number = 10;\n}\n\n// ✅ Extend pagination DTO\n// tasks/dto/get-tasks-filter.dto.ts\nimport { PaginationDto } from '../../common/dto/pagination.dto';\n\nexport class GetTasksFilterDto extends PaginationDto {\n @IsOptional()\n @IsEnum(TaskStatus)\n status?: TaskStatus;\n\n @IsOptional()\n @IsString()\n search?: string;\n}\n```\n\n### Step 5: Create Sort/Order DTO\n\n```typescript\n// ✅ OPTIONAL: Sortable fields\n// common/dto/sort.dto.ts\nimport { IsOptional, IsEnum, IsIn } from 'class-validator';\n\nexport class SortDto {\n @IsOptional()\n @IsEnum(['ASC', 'DESC'])\n order?: 'ASC' | 'DESC' = 'ASC';\n\n @IsOptional()\n @IsString()\n sortBy?: string;\n}\n\n// ✅ Strongly typed sort\n// tasks/dto/get-tasks-filter.dto.ts\nimport { TaskSortableFields } from '../task-sortable-fields.enum';\n\nexport class GetTasksFilterDto extends PaginationDto {\n @IsOptional()\n @IsEnum(TaskStatus)\n status?: TaskStatus;\n\n @IsOptional()\n @IsEnum(['ASC', 'DESC'])\n order?: 'ASC' | 'DESC' = 'ASC';\n\n @IsOptional()\n @IsEnum(TaskSortableFields)\n sortBy?: TaskSortableFields = TaskSortableFields.CREATED_AT;\n}\n\n// tasks/task-sortable-fields.enum.ts\nexport enum TaskSortableFields {\n TITLE = 'title',\n STATUS = 'status',\n CREATED_AT = 'createdAt',\n UPDATED_AT = 'updatedAt',\n}\n```\n\n### Step 6: Use in Repository Queries\n\n```typescript\n// ✅ REQUIRED: Type-safe queries in repository\n// tasks/tasks.repository.ts\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\n\nexport class TasksRepository extends Repository\u003cTask> {\n async getTasks(filterDto: GetTasksFilterDto, user: User): Promise\u003cTask[]> {\n const { status, search, page = 1, limit = 10 } = filterDto;\n const query = this.createQueryBuilder('task');\n\n query.where({ user });\n\n if (status) {\n // ✅ Type-safe: status is TaskStatus, not any string\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n // ✅ Type-safe: page and limit are numbers\n query.skip((page - 1) * limit).take(limit);\n\n return await query.getMany();\n }\n}\n```\n\n### Step 7: Add Date Range Filters\n\n```typescript\n// ✅ OPTIONAL: Date range DTO\n// common/dto/date-range.dto.ts\nimport { IsOptional, IsDate } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class DateRangeDto {\n @IsOptional()\n @Type(() => Date)\n @IsDate()\n startDate?: Date;\n\n @IsOptional()\n @Type(() => Date)\n @IsDate()\n endDate?: Date;\n}\n\n// ✅ Use date range in filter\n// tasks/dto/get-tasks-filter.dto.ts\nimport { DateRangeDto } from '../../common/dto/date-range.dto';\n\nexport class GetTasksFilterDto extends PaginationDto {\n @IsOptional()\n @IsEnum(TaskStatus)\n status?: TaskStatus;\n\n @IsOptional()\n @IsString()\n search?: string;\n\n @IsOptional()\n @Type(() => Date)\n @IsDate()\n createdAfter?: Date;\n\n @IsOptional()\n @Type(() => Date)\n @IsDate()\n createdBefore?: Date;\n}\n```\n\n## Installation\n\n```bash\n# Install required packages\nbun add class-validator class-transformer\n# or\nnpm install class-validator class-transformer\n```\n\n**Incorrect:**\n\n```typescript\n// tasks/tasks.controller.ts - Individual parameters 🚨\nimport { Controller, Get, Query } from '@nestjs/common';\n\n@Controller('tasks')\nexport class TasksController {\n @Get()\n findAll(\n @Query('status') status: string, // ❌ No validation\n @Query('search') search: string, // ❌ No validation\n @Query('page') page: string, // ❌ String, not number\n @Query('limit') limit: string, // ❌ String, not number\n ) {\n // ❌ Manual type conversion\n const pageNum = parseInt(page) || 1;\n const limitNum = parseInt(limit) || 10;\n\n // ❌ No type safety - status could be \"INVALID\"\n return this.tasksService.getTasks(status, search, pageNum, limitNum);\n }\n\n @Get()\n findAll(@Query() query: any) {\n // ❌ No type safety at all\n const status = query.status as string;\n const page = parseInt(query.page) || 1;\n const limit = parseInt(query.limit) || 10;\n const search = query.search;\n\n return this.tasksService.getTasks(status, search, page, limit);\n }\n}\n\n// tasks/tasks.service.ts 🚨\n@Injectable()\nexport class TasksService {\n // ❌ Too many parameters, no type safety\n async getTasks(\n status: string | undefined,\n search: string | undefined,\n page: number,\n limit: number,\n ) {\n // ❌ What values are valid for status? No hints.\n const query = this.repo.createQueryBuilder('task');\n\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n query.skip((page - 1) * limit).take(limit);\n\n return query.getMany();\n }\n}\n```\n\n**Correct:**\n\n```typescript\n// tasks/dto/get-tasks-filter.dto.ts - Filter DTO ✅\nimport { IsOptional, IsEnum, IsString, IsInt, Min, Max } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { TaskStatus } from '../task-status.enum';\n\nexport class GetTasksFilterDto {\n @IsOptional()\n @IsEnum(TaskStatus, {\n message: `Status must be one of: ${Object.values(TaskStatus).join(', ')}`,\n })\n status?: TaskStatus;\n\n @IsOptional()\n @IsString()\n search?: string;\n\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n page?: number = 1;\n\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n @Max(100)\n limit?: number = 10;\n}\n\n// common/dto/pagination.dto.ts - Reusable pagination ✅\nimport { IsOptional, IsInt, Min, Max } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class PaginationDto {\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n page?: number = 1;\n\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n @Max(100)\n limit?: number = 10;\n}\n\n// tasks/tasks.controller.ts - Clean controller ✅\nimport { Controller, Get, Query } from '@nestjs/common';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\n\n@Controller('tasks')\nexport class TasksController {\n constructor(private tasksService: TasksService) {}\n\n @Get()\n findAll(@Query() filterDto: GetTasksFilterDto) {\n // ✅ Single parameter, fully typed and validated\n return this.tasksService.getTasks(filterDto);\n }\n}\n\n// tasks/tasks.service.ts - Clean service ✅\nimport { Injectable } from '@nestjs/common';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\n\n@Injectable()\nexport class TasksService {\n constructor(private tasksRepository: TasksRepository) {}\n\n // ✅ Single DTO parameter\n async getTasks(filterDto: GetTasksFilterDto): Promise\u003cTask[]> {\n // ✅ Delegate to repository\n return this.tasksRepository.getTasks(filterDto);\n }\n}\n\n// tasks/tasks.repository.ts - Type-safe queries ✅\nimport { EntityRepository, Repository } from 'typeorm';\nimport { Task } from './task.entity';\nimport { GetTasksFilterDto } from './dto/get-tasks-filter.dto';\nimport { User } from '../auth/entities/user.entity';\n\n@EntityRepository(Task)\nexport class TasksRepository extends Repository\u003cTask> {\n async getTasks(filterDto: GetTasksFilterDto, user: User): Promise\u003cTask[]> {\n const { status, search, page = 1, limit = 10 } = filterDto;\n const query = this.createQueryBuilder('task');\n\n query.where({ user });\n\n // ✅ Type-safe: status is TaskStatus | undefined\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n // ✅ Type-safe: search is string | undefined\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n // ✅ Type-safe: page and limit are numbers\n query.skip((page - 1) * limit).take(limit);\n\n return await query.getMany();\n }\n}\n```\n\n## Advanced: Paginated Response\n\n```typescript\n// ✅ Return paginated results with metadata\n// common/dto/paginated-response.dto.ts\nexport class PaginatedResponseDto\u003cT> {\n data: T[];\n meta: {\n page: number;\n limit: number;\n total: number;\n totalPages: number;\n };\n}\n\n// tasks/tasks.repository.ts ✅\nexport class TasksRepository extends Repository\u003cTask> {\n async getTasksWithPagination(\n filterDto: GetTasksFilterDto,\n user: User,\n ): Promise\u003cPaginatedResponseDto\u003cTask>> {\n const { status, search, page = 1, limit = 10 } = filterDto;\n const query = this.createQueryBuilder('task');\n\n query.where({ user });\n\n if (status) {\n query.andWhere('task.status = :status', { status });\n }\n\n if (search) {\n query.andWhere('(task.title LIKE :search OR task.description LIKE :search)', {\n search: `%${search}%`,\n });\n }\n\n const [data, total] = await query\n .skip((page - 1) * limit)\n .take(limit)\n .getManyAndCount();\n\n return {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages: Math.ceil(total / limit),\n },\n };\n }\n}\n```\n\n## Advanced: Composable Filter DTOs\n\n```typescript\n// ✅ Composable DTOs with intersections\n// common/dto/base-filter.dto.ts\nexport class BaseFilterDto {\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n page?: number = 1;\n\n @IsOptional()\n @Type(() => Number)\n @IsInt()\n @Min(1)\n @Max(100)\n limit?: number = 10;\n}\n\n// tasks/dto/get-tasks-filter.dto.ts ✅\nimport { BaseFilterDto } from '../../common/dto/base-filter.dto';\n\nexport class GetTasksFilterDto extends BaseFilterDto {\n @IsOptional()\n @IsEnum(TaskStatus)\n status?: TaskStatus;\n\n @IsOptional()\n @IsString()\n search?: string;\n\n @IsOptional()\n @Type(() => Date)\n @IsDate()\n from?: Date;\n\n @IsOptional()\n @Type(() => Date)\n @IsDate()\n to?: Date;\n}\n```\n\n## Advanced: Validation Groups\n\n```typescript\n// ✅ Different validation for different scenarios\n// tasks/dto/get-tasks-filter.dto.ts\nimport { ValidateIf } from 'class-validator';\n\nexport class GetTasksFilterDto {\n @IsOptional()\n @IsEnum(TaskStatus)\n status?: TaskStatus;\n\n // ✅ Only validate if search is present\n @IsOptional()\n @IsString()\n @MinLength(3, { message: 'Search must be at least 3 characters' })\n search?: string;\n\n // ✅ Validate 'from' only if 'to' is also present\n @IsOptional()\n @ValidateIf((o) => o.to !== undefined)\n @IsDate()\n from?: Date;\n\n @IsOptional()\n @ValidateIf((o) => o.from !== undefined)\n @IsDate()\n to?: Date;\n}\n```\n\n## Best Practices Summary\n\n| Practice | Why |\n|----------|-----|\n| Use filter DTOs | Single validated parameter |\n| Add @Type() decorator | Auto-transform strings to numbers/dates |\n| Use @IsOptional() | Parameters are optional by default |\n| Add default values | Cleaner code with default pagination |\n| Create reusable base DTOs | DRY principle for common filters |\n| Extend base DTOs | Composable filter combinations |\n\n**Sources:**\n- [NestJS Query Parameters Documentation](https://docs.nestjs.com/controllers#query-parameters)\n- [class-validator Documentation](https://github.com/typestack/class-validator)\n- [class-transformer Documentation](https://github.com/typestack/class-transformer)\n- [arielweinberger/nestjs-recipe](https://github.com/arielweinberger/nestjs-recipe) - Production-ready NestJS application\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14617,"content_sha256":"976a2e50ebca910ff1315175219f90be3f383fdf33cf79b1925a1c929a8a514a"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"NestJS Best Practices","type":"text"}]},{"type":"paragraph","content":[{"text":"Comprehensive guide for building production-ready NestJS applications. Contains 26 rules across 13 categories, covering security, architecture, performance, validation, database operations, authentication, and advanced patterns.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Apply","type":"text"}]},{"type":"paragraph","content":[{"text":"Reference these guidelines when:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Writing new NestJS modules, controllers, or services","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implementing authentication and authorization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setting up Prisma database operations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creating DTOs and validation pipes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding middleware or guards","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implementing error handling and logging","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reviewing NestJS code for best practices","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Refactoring existing NestJS applications","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rule Categories by Priority","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Priority","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Impact","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prefix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Security","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"security-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Performance","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"performance-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Architecture","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"architecture-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error Handling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"error-handling-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Validation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validation-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Database","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"database-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Authentication","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"auth-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"api-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"9","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Configuration","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"config-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Testing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"testing-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"11","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deployment","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deployment-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"12","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Middleware","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"middleware-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"13","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Advanced","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"advanced-","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Security (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"security-cors-whitelist","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Enable CORS with whitelist origins only","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"security-dependency-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Regular dependency security audits with bun","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"security-helmet-headers","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use Helmet middleware for security headers","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Performance (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"performance-redis-caching","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Cache frequently used data with Redis","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Architecture (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"architecture-short-functions","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Keep functions short and single purpose","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"architecture-feature-modules","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Organize code by feature modules","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"architecture-no-dead-code","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Remove unused code and dependencies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"architecture-thin-controllers","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Single responsibility - separate controller and service","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"architecture-naming-conventions","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use consistent naming conventions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"architecture-event-driven","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use event-driven architecture for loose coupling","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Error Handling (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-handling-exception-filter","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Enable global exception filter","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-handling-structured-logging","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Implement proper logging strategy","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Validation (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"validation-custom-pipes","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Create custom pipes for query parameter transformation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"validation-dto-validation","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Validate all inputs with DTOs and ValidationPipe","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Database (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"database-parameterized-queries","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use parameterized queries to prevent SQL injection (Prisma v7)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. Authentication (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth-password-hashing","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use Bun's built-in Crypto for secure password hashing (argon2/bcrypt)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth-route-guards","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use guards for route protection","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"8. API (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"api-cursor-pagination","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use cursor-based pagination for large datasets","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"api-swagger-docs","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Generate Swagger/OpenAPI documentation","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"9. Configuration (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"config-no-secrets","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Never hardcode secrets - use environment variables","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"10. Testing (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"testing-unit-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Write comprehensive unit tests","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"11. Deployment (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deployment-health-checks","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Implement health check endpoints","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"12. Middleware (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"middleware-compression","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Enable compression middleware for responses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"middleware-rate-limiting","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Implement rate limiting for all routes","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"13. Advanced (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced-lazy-loading","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Lazy load non-critical modules","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advanced-scheduled-tasks","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use @nestjs/schedule for cron jobs and scheduled tasks","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use","type":"text"}]},{"type":"paragraph","content":[{"text":"Read individual rule files for detailed explanations and code examples:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"rules/security-cors-whitelist.md\nrules/auth-route-guards.md\nrules/validation-dto-validation.md\nrules/_sections.md","type":"text"}]},{"type":"paragraph","content":[{"text":"Each rule file contains:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"For AI Agents\" section with step-by-step implementation instructions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Quick Reference Checklist for code review","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"❌ WRONG vs ✅ CORRECT patterns with file locations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Installation commands using ","type":"text"},{"text":"bun add","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Comprehensive code examples for NestJS + Prisma","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security and performance considerations","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Agent-Friendly Format","type":"text"}]},{"type":"paragraph","content":[{"text":"All rules are optimized for AI agents with:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Explicit file locations (e.g., ","type":"text"},{"text":"src/users/users.service.ts","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Step-by-step \"For AI Agents\" sections","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ CORRECT vs ❌ WRONG pattern comparisons","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Quick Reference Checklists","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Clear installation instructions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Technology Stack","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Framework","type":"text","marks":[{"type":"strong"}]},{"text":": NestJS with Express/Fastify adapter","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Runtime","type":"text","marks":[{"type":"strong"}]},{"text":": Bun (native crypto, scheduling)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Database","type":"text","marks":[{"type":"strong"}]},{"text":": Prisma v7 (sql template tag, interactive transactions)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validation","type":"text","marks":[{"type":"strong"}]},{"text":": class-validator + class-transformer","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security","type":"text","marks":[{"type":"strong"}]},{"text":": Helmet, CORS whitelisting, rate limiting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scheduling","type":"text","marks":[{"type":"strong"}]},{"text":": @nestjs/schedule with cron/interval decorators","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Events","type":"text","marks":[{"type":"strong"}]},{"text":": EventEmitter2 for loose coupling","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Full Compiled Document","type":"text"}]},{"type":"paragraph","content":[{"text":"For the complete guide with all rules expanded: ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Build Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cd packages/nestjs-best-practices-build\nbun install\nbun run build # Generate AGENTS.md\nbun run validate # Validate rule files\nbun run dev # Build and validate","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Statistics","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"13 sections","type":"text","marks":[{"type":"strong"}]},{"text":" covering all aspects of NestJS development","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"26 rules","type":"text","marks":[{"type":"strong"}]},{"text":" prioritized by impact (CRITICAL, HIGH, MEDIUM)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prisma v7","type":"text","marks":[{"type":"strong"}]},{"text":" compatible database patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bun runtime","type":"text","marks":[{"type":"strong"}]},{"text":" native features (Crypto.hashPassword, @nestjs/schedule)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent-optimized","type":"text","marks":[{"type":"strong"}]},{"text":" for automated code generation and review","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"nestjs-best-practices","author":"@skillopedia","source":{"stars":12,"repo_name":"agent-skills","origin_url":"https://github.com/xirothedev/agent-skills/blob/HEAD/skills/nestjs-best-practices/SKILL.md","repo_owner":"xirothedev","body_sha256":"76ae538bd65ce2973ff2e73a26a5f6e8fed221d997362961b8a639fba659f35a","cluster_key":"64932ffd1d0099859f1d1c01cdd442a51a7f3b9d47a972605afe683f6415faf8","clean_bundle":{"format":"clean-skill-bundle-v1","source":"xirothedev/agent-skills/skills/nestjs-best-practices/SKILL.md","attachments":[{"id":"d338dc2f-472d-57c0-b46f-b18f4af00013","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d338dc2f-472d-57c0-b46f-b18f4af00013/attachment.md","path":"AGENTS.md","size":92382,"sha256":"beaaca24a70f563495907468a0f70aa4cce8552a05610069308745ddace5c06d","contentType":"text/markdown; charset=utf-8"},{"id":"74dbbdca-8d93-55a0-bdff-83f9a8130471","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/74dbbdca-8d93-55a0-bdff-83f9a8130471/attachment.md","path":"README.md","size":6053,"sha256":"a1c30d834c7df41ed18b5f8a16ab07ab54f7eec548ad2005e6ba211c8674b0e2","contentType":"text/markdown; charset=utf-8"},{"id":"9c31e30d-8b9f-5056-aa75-f7a35df540c6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9c31e30d-8b9f-5056-aa75-f7a35df540c6/attachment.json","path":"metadata.json","size":686,"sha256":"1baba55eeea6051c3a25539549cd2d11be9be0ffba73f893cff577a31ac2b0e5","contentType":"application/json; charset=utf-8"},{"id":"9222b61a-ef32-5a74-a026-7b8744390776","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9222b61a-ef32-5a74-a026-7b8744390776/attachment.md","path":"rules/_sections.md","size":3021,"sha256":"b456bf6b8ad98e9472e9494d87119e6f1cf77a362b3975e2b2d90e726994940d","contentType":"text/markdown; charset=utf-8"},{"id":"29e9ddda-75e1-55b6-a7b2-ee31b8b77abb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/29e9ddda-75e1-55b6-a7b2-ee31b8b77abb/attachment.md","path":"rules/_template.md","size":630,"sha256":"89cf43dbf734562437b6c30600b02af4b66d7a69528c4167f6409c0298058e89","contentType":"text/markdown; charset=utf-8"},{"id":"44df17cc-e867-5c1e-a055-1d9a718802d1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/44df17cc-e867-5c1e-a055-1d9a718802d1/attachment.md","path":"rules/advanced-lazy-loading.md","size":9514,"sha256":"343ffbce7436e4db4d6ada4c5d3572ca68b580ebbff6f7041a6037d1a48bd06b","contentType":"text/markdown; charset=utf-8"},{"id":"0ab4d07f-1711-564b-8b47-29fd9f5c6335","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0ab4d07f-1711-564b-8b47-29fd9f5c6335/attachment.md","path":"rules/advanced-scheduled-tasks.md","size":13265,"sha256":"0456aec62e11ab522c1701c1e94aec92895886a3d6aa2e0d493c1ef22f0d693a","contentType":"text/markdown; charset=utf-8"},{"id":"2b602b96-4861-5c4f-bf12-47b14f415219","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2b602b96-4861-5c4f-bf12-47b14f415219/attachment.md","path":"rules/api-cursor-pagination.md","size":16566,"sha256":"89d975963c2ccfa5aa763d5985418a5c2e9b1e2f62f3a2999ad37413d1ba9e3d","contentType":"text/markdown; charset=utf-8"},{"id":"b0d1e396-e914-5488-85f2-f29c81863826","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b0d1e396-e914-5488-85f2-f29c81863826/attachment.md","path":"rules/api-swagger-docs.md","size":1661,"sha256":"e07fa5173079d3b6b221425899a13aacb6e1c6dbb8dfa15f624f109c28f8b483","contentType":"text/markdown; charset=utf-8"},{"id":"4f136f6e-872c-5af5-91d7-a5b8f239c1a2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4f136f6e-872c-5af5-91d7-a5b8f239c1a2/attachment.md","path":"rules/architecture-enum-classes.md","size":13500,"sha256":"9256e579fa958f0de4cd6bfaef996053eeef794516d9e739b21aca090f3dedbb","contentType":"text/markdown; charset=utf-8"},{"id":"b379c2e9-e9d7-53ab-a609-7a07c5fb6a53","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b379c2e9-e9d7-53ab-a609-7a07c5fb6a53/attachment.md","path":"rules/architecture-event-driven.md","size":18579,"sha256":"a8cc07c82eedf33f19c541056b4468e8f0268836251fc00e839cddb93c71d3d5","contentType":"text/markdown; charset=utf-8"},{"id":"4c46ed62-fd20-5636-ad1f-dcf71661f28a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4c46ed62-fd20-5636-ad1f-dcf71661f28a/attachment.md","path":"rules/architecture-feature-modules.md","size":9921,"sha256":"5497d91939c97496ae1652e7033c0254aae8dc29f78bdb4b292621847c13fd7f","contentType":"text/markdown; charset=utf-8"},{"id":"d6a7515d-13c2-550a-88a4-1722c4cbbb6c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d6a7515d-13c2-550a-88a4-1722c4cbbb6c/attachment.md","path":"rules/architecture-naming-conventions.md","size":917,"sha256":"4de25d277c82563e23121439cdc98110af2cd4b461f094004ea67589da8574d2","contentType":"text/markdown; charset=utf-8"},{"id":"3a71ca8c-5cd2-5c0d-ae90-3bccc12debbd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3a71ca8c-5cd2-5c0d-ae90-3bccc12debbd/attachment.md","path":"rules/architecture-no-dead-code.md","size":1478,"sha256":"5dc23fdb83a220152972e211f274a0da1e07fdc961b0157426119c8d2f8bf02e","contentType":"text/markdown; charset=utf-8"},{"id":"55342c1a-e17c-565b-80ab-893dc6c69046","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/55342c1a-e17c-565b-80ab-893dc6c69046/attachment.md","path":"rules/architecture-short-functions.md","size":1374,"sha256":"882a22d7ff003d020856d55adfc0efa0d70f98b360ebb16f33650411c1f1a68a","contentType":"text/markdown; charset=utf-8"},{"id":"15a4bf4c-e6e2-5c43-9063-400690819438","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/15a4bf4c-e6e2-5c43-9063-400690819438/attachment.md","path":"rules/architecture-thin-controllers.md","size":13830,"sha256":"b26d546f3494cfb40d212be209638bb3d72c668f43d1ce83c7b33e01151fd6e8","contentType":"text/markdown; charset=utf-8"},{"id":"26c342e2-d8de-56c5-8e51-9c7df4d415fd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/26c342e2-d8de-56c5-8e51-9c7df4d415fd/attachment.md","path":"rules/auth-custom-decorators.md","size":14316,"sha256":"e857c2c93ad7b0456ae25753e13f423395d49ca58d80be0d986b6f5f21d7f3f7","contentType":"text/markdown; charset=utf-8"},{"id":"15666c92-57c9-541c-8ece-7c8f827058f0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/15666c92-57c9-541c-8ece-7c8f827058f0/attachment.md","path":"rules/auth-password-hashing.md","size":22387,"sha256":"7296c0b260291d1752bb2cda54ea6b50372a7d1e461b15ef51c088e00cd57ca6","contentType":"text/markdown; charset=utf-8"},{"id":"a4b05ca5-df67-5a7a-971d-b6c72e7c88c2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a4b05ca5-df67-5a7a-971d-b6c72e7c88c2/attachment.md","path":"rules/auth-route-guards.md","size":13226,"sha256":"8bd25168bca99df19dd6942558feaf4b9ebc896a627aff816a202012271ec62d","contentType":"text/markdown; charset=utf-8"},{"id":"7b97f78d-4d65-582c-98a4-5986a472f38b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7b97f78d-4d65-582c-98a4-5986a472f38b/attachment.md","path":"rules/config-no-secrets.md","size":6271,"sha256":"126adb7a8cbc31b31d689ec4740fb9f591c49f52f03416ef0cbebc645202f64a","contentType":"text/markdown; charset=utf-8"},{"id":"344ba086-f468-5268-81c0-6c82b1d27773","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/344ba086-f468-5268-81c0-6c82b1d27773/attachment.md","path":"rules/database-parameterized-queries.md","size":13205,"sha256":"a59b3356dc81a573befb76894b9082b89739aece2d837a12b4606a22af8ae759","contentType":"text/markdown; charset=utf-8"},{"id":"5b2f64fd-7cb9-517e-b11b-bfe3ddeb0e1d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5b2f64fd-7cb9-517e-b11b-bfe3ddeb0e1d/attachment.md","path":"rules/database-repository-pattern.md","size":18283,"sha256":"97fa187f6090b09ef9388e598f20054e1ec902f13bd24bdc402da2a970b357a3","contentType":"text/markdown; charset=utf-8"},{"id":"e2460f72-8b2b-5c58-8c2a-fc1fb9188ff2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e2460f72-8b2b-5c58-8c2a-fc1fb9188ff2/attachment.md","path":"rules/deployment-health-checks.md","size":1180,"sha256":"f793433b7ebee2c1796a25e7de5149512e94fed05c2d2a85da33919b9771b1c3","contentType":"text/markdown; charset=utf-8"},{"id":"a702a549-bc20-5491-a706-ddbdef025b59","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a702a549-bc20-5491-a706-ddbdef025b59/attachment.md","path":"rules/error-handling-exception-filter.md","size":14639,"sha256":"d494e734dac410a837613fdf828a78125ea3947acc1769320d4435cf628b6b21","contentType":"text/markdown; charset=utf-8"},{"id":"7f46610d-3d89-5b9c-af65-7292f29f1225","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7f46610d-3d89-5b9c-af65-7292f29f1225/attachment.md","path":"rules/error-handling-logger-context.md","size":15782,"sha256":"7b9ea4d3f3354e818af0233742ef67dc1e43a1720dbc74e548d81af316d0c03b","contentType":"text/markdown; charset=utf-8"},{"id":"78f52670-80f4-5871-b4c8-b848909c24aa","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/78f52670-80f4-5871-b4c8-b848909c24aa/attachment.md","path":"rules/error-handling-structured-logging.md","size":2053,"sha256":"dc016570c493649156028c851c70b25eeb76ec626110b4620cd41b561dbaa433","contentType":"text/markdown; charset=utf-8"},{"id":"5d7f566e-e1f1-5875-b285-78735b4e84ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5d7f566e-e1f1-5875-b285-78735b4e84ee/attachment.md","path":"rules/interceptors-response-transform.md","size":14360,"sha256":"d25d12339f751a6a8159e445d3ab9b37645e09fe1f47c991dd195f6d6e6c6a5f","contentType":"text/markdown; charset=utf-8"},{"id":"81d8eab8-011c-5545-8947-b3680097c46d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/81d8eab8-011c-5545-8947-b3680097c46d/attachment.md","path":"rules/middleware-compression.md","size":988,"sha256":"fa973cd5daa08135ef1434b35aa0fcfa489199014d246fcda1bf5ce8d615143a","contentType":"text/markdown; charset=utf-8"},{"id":"d49b006d-b4f3-5290-a457-63494a30788e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d49b006d-b4f3-5290-a457-63494a30788e/attachment.md","path":"rules/middleware-rate-limiting.md","size":1034,"sha256":"6f5ef2f489a18f9ef74b97e4b0da079246ffa1c34921e53dd474706d78082efd","contentType":"text/markdown; charset=utf-8"},{"id":"8dd28809-e51f-5607-b1ef-42d311358ac0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8dd28809-e51f-5607-b1ef-42d311358ac0/attachment.md","path":"rules/performance-redis-caching.md","size":1484,"sha256":"c76928ebdccca77404c562d754cf3566d22ffb79515941e82e6e15c9f1ec7d67","contentType":"text/markdown; charset=utf-8"},{"id":"4859c384-e314-5218-90f5-f71b2d711344","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4859c384-e314-5218-90f5-f71b2d711344/attachment.md","path":"rules/security-cors-whitelist.md","size":779,"sha256":"e5e0ad92073b12523041a984ff6de4b7cb9789d8cfa0c3db6329e70d0817b7de","contentType":"text/markdown; charset=utf-8"},{"id":"8c1618f5-474b-5ec1-8d9d-1d3f59551bfd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8c1618f5-474b-5ec1-8d9d-1d3f59551bfd/attachment.md","path":"rules/security-dependency-audit.md","size":1250,"sha256":"2b127f932e1e93ae961b09f25d4393bbe3d656bbef3038efc8a78c127a53dac7","contentType":"text/markdown; charset=utf-8"},{"id":"196cd5fc-924d-54c8-bd62-ae7691e76219","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/196cd5fc-924d-54c8-bd62-ae7691e76219/attachment.md","path":"rules/security-helmet-headers.md","size":3071,"sha256":"bc85272b408bcd9cd5eef8c426b830f91161748a196b3a52f9898f36fd5ed75c","contentType":"text/markdown; charset=utf-8"},{"id":"ffac46f3-ef03-537c-9260-8caeb37e0f59","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ffac46f3-ef03-537c-9260-8caeb37e0f59/attachment.md","path":"rules/testing-unit-tests.md","size":1425,"sha256":"fe5a3c16777134f582412b399aabdecc1bf7e23d61c7266f3e143dd469676820","contentType":"text/markdown; charset=utf-8"},{"id":"a7f9ca60-2b07-576d-b960-5c926fd592d9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a7f9ca60-2b07-576d-b960-5c926fd592d9/attachment.md","path":"rules/validation-custom-pipes.md","size":18532,"sha256":"71dd99a6be2a625277eaac3372fd98b3c2881dc1af6f8139bbdcb4144911406e","contentType":"text/markdown; charset=utf-8"},{"id":"2bc2d296-1ea8-51a5-a762-5888e34c1955","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2bc2d296-1ea8-51a5-a762-5888e34c1955/attachment.md","path":"rules/validation-dto-validation.md","size":6088,"sha256":"38860f236f9c1d02ca0e9519358255962f2fbecc567a41210738cff99c3e1843","contentType":"text/markdown; charset=utf-8"},{"id":"0445a79c-12d4-54e9-8d80-dd3aa3646ea3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0445a79c-12d4-54e9-8d80-dd3aa3646ea3/attachment.md","path":"rules/validation-filter-dtos.md","size":14617,"sha256":"976a2e50ebca910ff1315175219f90be3f383fdf33cf79b1925a1c929a8a514a","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"c59a38afbdb0b520e7b29a44c56b6e31ba132780174e0f0a51ddfc46739f2dc7","attachment_count":37,"text_attachments":37,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/nestjs-best-practices/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"security","metadata":{"author":"xiro","version":"1.0.0"},"import_tag":"clean-skills-v1","description":"NestJS best practices and patterns for building scalable, maintainable backend applications. This skill should be used when writing, reviewing, or refactoring NestJS code to ensure proper architecture, security, performance, and code quality. Triggers on tasks involving NestJS modules, controllers, services, guards, pipes, middleware, Prisma database operations, authentication, or any NestJS-specific patterns."}},"renderedAt":1782989119406}

NestJS Best Practices Comprehensive guide for building production-ready NestJS applications. Contains 26 rules across 13 categories, covering security, architecture, performance, validation, database operations, authentication, and advanced patterns. When to Apply Reference these guidelines when: - Writing new NestJS modules, controllers, or services - Implementing authentication and authorization - Setting up Prisma database operations - Creating DTOs and validation pipes - Adding middleware or guards - Implementing error handling and logging - Reviewing NestJS code for best practices - Refa…