// 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';
// ✅ Good: Type-safe response
async getUser(id: number): Promise<ApiResponse<UserDto>> {
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"
// }
// ✅ 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"
// }
// ✅ Good: Void operations
async deleteUser(id: number): Promise<void> {
await this.userService.delete(id)
// No return needed
}
// Response:
// {
// success: true,
// code: "OK",
// message: "success",
// data: null,
// timestamp: "2025-11-20T12:34:56.789Z"
// }
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)
}
// ✅ 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"
// }
// ✅ 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"
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);
}
}
// 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)
}
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);
}
}
// ✅ 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
)
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
}
}
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
}
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;
}
}
}
// ✅ 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;
}
import { ConfigService } from '@nestjs/config';
import { EnvironmentVariables } from '../config/env.validation';
export class SomeService {
constructor(private configService: ConfigService<EnvironmentVariables>) {}
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,
});
}
}
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);
});
});
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);
});
});
// ❌ Bad: Manually creating ApiResponse
async getUser(id: number): Promise<ApiResponse<User>> {
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<User> {
return this.userService.findById(id)
// Interceptor auto-wraps in ApiResponse
}
// ❌ 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
}
// ❌ Bad: Conflicting guards
@UseGuards(PublicGuard, JwtAuthGuard) // Contradiction!
@Get('public-endpoint')
// ✅ Good: Use @Public() decorator
@Public()
@Get('public-endpoint')
When creating new endpoints or updating existing ones:
@UseGuards(RateLimitGuard) to auth/sensitive endpoints@UseGuards(MfaGuard) to destructive operations# 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"
BEFORE_AFTER.mdDEPLOYMENT_CHECKLIST.mdREFACTOR_SUMMARY.md