# Developer Guide - New API Patterns ## Quick Reference ### Importing New Guards & Interceptors ```typescript // Rate limiting import { RateLimitGuard } from '@box/common/guards/rate-limit.guard'; // MFA enforcement import { MfaGuard } from '@box/common/guards/mfa.guard'; // Response types import { ApiResponse, PaginatedApiResponse, } from '@box/common/interfaces/api-response.interface'; ``` --- ## Response Typing Best Practices ### Basic Response ```typescript // ✅ Good: Type-safe response async getUser(id: number): Promise> { const user = await this.userService.findById(id) // Response interceptor automatically wraps in ApiResponse return user } // Response: // { // success: true, // code: "OK", // message: "success", // data: { id: 1, username: "admin", ... }, // timestamp: "2025-11-20T12:34:56.789Z" // } ``` ### Paginated Response ```typescript // ✅ Good: Paginated response type async listUsers( query: SearchUserDto ): Promise<{ list: UserDto[]; total: number }> { const { list, total } = await this.userService.list(query) return { list, total } } // Response (auto-wrapped): // { // success: true, // code: "OK", // message: "success", // data: { // list: [...], // total: 100 // }, // timestamp: "2025-11-20T12:34:56.789Z" // } ``` ### Void Response ```typescript // ✅ Good: Void operations async deleteUser(id: number): Promise { await this.userService.delete(id) // No return needed } // Response: // { // success: true, // code: "OK", // message: "success", // data: null, // timestamp: "2025-11-20T12:34:56.789Z" // } ``` --- ## Error Handling Patterns ### Standard HTTP Exceptions ```typescript import { BadRequestException, NotFoundException, UnauthorizedException, ForbiddenException, ConflictException } from '@nestjs/common' // ✅ Good: Use standard exceptions async updateUser(id: number, dto: UpdateUserDto) { const user = await this.userService.findById(id) if (!user) { throw new NotFoundException('User not found') // → HTTP 404, code: "NOT_FOUND" } if (user.username === 'admin' && dto.username !== 'admin') { throw new ForbiddenException('Cannot rename admin user') // → HTTP 403, code: "FORBIDDEN" } return this.userService.update(id, dto) } ``` ### Custom Error Codes ```typescript // ✅ Good: Custom error codes for specific cases async enable2FA(user: User, dto: Enable2FADto) { if (user.twoFA) { throw new BadRequestException({ statusCode: 400, message: '2FA already enabled', code: 'TWO_FA_ALREADY_ENABLED' }) } // Business logic... } // Response: // HTTP 400 Bad Request // { // success: false, // code: "TWO_FA_ALREADY_ENABLED", // message: "2FA already enabled", // data: null, // timestamp: "2025-11-20T12:34:56.789Z" // } ``` ### Validation Errors ```typescript // ✅ Good: class-validator errors auto-formatted class CreateUserDto { @IsNotEmpty() @Length(3, 20) username: string; @IsEmail() email: string; @IsStrongPassword() password: string; } // Invalid request: // → HTTP 400, message: "username must be longer than or equal to 3 characters, email must be an email" ``` --- ## Rate Limiting Patterns ### Apply to Sensitive Endpoints ```typescript import { RateLimitGuard } from '@box/common/guards/rate-limit.guard'; @Controller('auth') export class AuthController { // ✅ Good: Rate limit login attempts @UseGuards(RateLimitGuard, LocalAuthGuard) @Post('login') async login(@AuthUser() user: User) { return this.authService.login(user); } // ✅ Good: Rate limit password reset @UseGuards(RateLimitGuard) @Post('reset-password') async resetPassword(@Body() dto: ResetPasswordDto) { return this.authService.resetPassword(dto); } // ❌ Bad: Don't rate limit read operations @Get('permission') // No rate limit async getPermission(@AuthUser() user: User) { return this.authService.getPermission(user); } } ``` ### Custom Rate Limits (Future Enhancement) ```typescript // For future: configurable rate limits per endpoint @UseGuards(RateLimitGuard) @RateLimit({ limit: 5, window: 300 }) // 5 requests per 5 minutes @Post('send-verification-email') async sendVerificationEmail(@AuthUser() user: User) { return this.emailService.sendVerification(user) } ``` --- ## MFA Guard Patterns ### Protect Sensitive Operations ```typescript import { MfaGuard } from '@box/common/guards/mfa.guard'; @Controller('users') export class UserController { // ✅ Good: Require MFA for destructive operations @UseGuards(JwtAuthGuard, MfaGuard) @Delete(':id') async deleteUser(@Param('id') id: number) { return this.userService.delete(id); } // ✅ Good: Require MFA for privilege escalation @UseGuards(JwtAuthGuard, MfaGuard) @Post(':id/grant-admin') async grantAdmin(@Param('id') id: number) { return this.userService.grantAdmin(id); } // ❌ Bad: Don't use MfaGuard for read operations @UseGuards(JwtAuthGuard) // No MfaGuard @Get(':id') async getUser(@Param('id') id: number) { return this.userService.get(id); } } ``` ### Guard Execution Order ```typescript // ✅ Good: Correct guard order @UseGuards( RateLimitGuard, // 1. Check rate limit first LocalAuthGuard, // 2. Then authenticate MfaGuard // 3. Finally check MFA ) @Post('sensitive-action') async sensitiveAction() { // All guards passed } // ❌ Bad: Wrong order @UseGuards( MfaGuard, // ❌ Will fail - user not authenticated yet LocalAuthGuard ) ``` --- ## Correlation ID Usage ### Accessing Correlation ID ```typescript import type { FastifyRequest } from 'fastify' @Post('process') async processData(@Req() req: FastifyRequest) { const correlationId = (req as any).correlationId this.logger.log(`[${correlationId}] Processing started`) try { const result = await this.service.process() this.logger.log(`[${correlationId}] Processing completed`) return result } catch (error) { this.logger.error(`[${correlationId}] Processing failed`, error.stack) throw error } } ``` ### Passing to External Services ```typescript async callExternalApi(@Req() req: FastifyRequest) { const correlationId = (req as any).correlationId // ✅ Good: Pass correlation ID to downstream services const response = await this.httpService.post( 'https://api.example.com/endpoint', { data: '...' }, { headers: { 'x-correlation-id': correlationId } } ) return response.data } ``` --- ## Logging Best Practices ### Structured Logging with Context ```typescript import { Logger } from '@nestjs/common'; export class UserService { private readonly logger = new Logger(UserService.name); async createUser(dto: CreateUserDto, correlationId?: string) { const context = correlationId ? `[${correlationId}]` : ''; this.logger.log(`${context} Creating user: ${dto.username}`); try { const user = await this.userRepo.create(dto); this.logger.log(`${context} User created: ${user.id}`); return user; } catch (error) { this.logger.error( `${context} Failed to create user: ${dto.username}`, error.stack, ); throw error; } } } ``` ### Error Logging Levels ```typescript // ✅ Good: Appropriate log levels try { const user = await this.userService.findById(id); if (!user) { this.logger.warn(`User not found: ${id}`); // User error throw new NotFoundException(); } return user; } catch (error) { if (error instanceof NotFoundException) { // Don't log - already logged as warning } else { this.logger.error('Unexpected error', error.stack); // System error } throw error; } ``` --- ## Configuration Access ### Type-Safe Config ```typescript import { ConfigService } from '@nestjs/config'; import { EnvironmentVariables } from '../config/env.validation'; export class SomeService { constructor(private configService: ConfigService) {} async someMethod() { // ✅ Good: Type-safe config access const jwtSecret = this.configService.get('JWT_SECRET', { infer: true }); const jwtExpiry = this.configService.get('JWT_EXPIRES_IN_SECONDS', { infer: true, }); // No need for || fallbacks - validation ensures they exist this.jwtService.sign(payload, { secret: jwtSecret, expiresIn: jwtExpiry, }); } } ``` --- ## Testing Patterns ### Unit Tests with New Response Format ```typescript describe('UserController', () => { it('should return user in ApiResponse format', async () => { const result = await controller.getUser(1); // ✅ Good: Test unwrapped data (interceptor adds wrapper) expect(result).toEqual({ id: 1, username: 'test', // ... }); }); it('should throw proper HTTP exception', async () => { // ✅ Good: Test exception type and status await expect(controller.getUser(999)).rejects.toThrow(NotFoundException); }); }); ``` ### Integration Tests ```typescript describe('Auth E2E', () => { it('should enforce rate limiting', async () => { // Send 10 requests (should succeed) for (let i = 0; i < 10; i++) { const response = await request(app.getHttpServer()) .post('/auth/login') .send({ username: 'test', password: 'wrong' }); expect([401, 400]).toContain(response.status); } // 11th request should be rate limited const response = await request(app.getHttpServer()) .post('/auth/login') .send({ username: 'test', password: 'wrong' }); expect(response.status).toBe(429); expect(response.body).toMatchObject({ success: false, code: 'RATE_LIMITED', }); }); it('should include correlation ID in response', async () => { const correlationId = 'test-correlation-123'; const response = await request(app.getHttpServer()) .get('/auth/permission') .set('x-request-id', correlationId) .set('Authorization', `Bearer ${token}`); expect(response.headers['x-request-id']).toBe(correlationId); expect(response.headers['x-correlation-id']).toBe(correlationId); }); }); ``` --- ## Common Pitfalls to Avoid ### ❌ Don't Return Raw ApiResponse ```typescript // ❌ Bad: Manually creating ApiResponse async getUser(id: number): Promise> { const user = await this.userService.findById(id) return { success: true, code: 'OK', message: 'User found', data: user, timestamp: new Date().toISOString() } } // ✅ Good: Let interceptor wrap response async getUser(id: number): Promise { return this.userService.findById(id) // Interceptor auto-wraps in ApiResponse } ``` ### ❌ Don't Swallow Errors ```typescript // ❌ Bad: Swallowing errors async updateUser(id: number, dto: UpdateUserDto) { try { return await this.userService.update(id, dto) } catch (error) { return null // ❌ Error hidden! } } // ✅ Good: Let errors propagate async updateUser(id: number, dto: UpdateUserDto) { return this.userService.update(id, dto) // Exception filter handles errors } ``` ### ❌ Don't Mix Guard Types ```typescript // ❌ Bad: Conflicting guards @UseGuards(PublicGuard, JwtAuthGuard) // Contradiction! @Get('public-endpoint') // ✅ Good: Use @Public() decorator @Public() @Get('public-endpoint') ``` --- ## Migration Checklist for Developers When creating new endpoints or updating existing ones: - [ ] Use proper HTTP status codes (throw exceptions, don't return error objects) - [ ] Return data directly (let ResponseInterceptor wrap it) - [ ] Add `@UseGuards(RateLimitGuard)` to auth/sensitive endpoints - [ ] Add `@UseGuards(MfaGuard)` to destructive operations - [ ] Use TypeScript generics for type-safe responses - [ ] Include correlation ID in logs for debugging - [ ] Write tests for both success and error cases - [ ] Document custom error codes in API docs --- ## Quick Command Reference ```bash # Generate Prisma clients pnpm prisma:generate # Build application pnpm build:mgnt # Run development server pnpm dev:mgnt # Run production server pnpm start:mgnt # Check for errors pnpm build:mgnt && echo "✅ No errors" ``` --- ## Need Help? - **Response format questions**: See `BEFORE_AFTER.md` - **Deployment steps**: See `DEPLOYMENT_CHECKLIST.md` - **Architecture overview**: See `REFACTOR_SUMMARY.md` - **Code examples**: This document