코딩일상

[nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[exception-filters] 본문

개발 공부/nest.js

[nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[exception-filters]

solutionMan 2025. 12. 16. 23:06
반응형

1. Exception Layer 

1.1 Built-in Exception Layer

NestJS는 내장 예외 레이어를 제공하여 애플리케이션 전체에서 처리되지 않은 모든 예외를 자동으로 처리합니다.

동작 방식:

  • 애플리케이션 코드에서 처리되지 않은 예외 발생
  • Built-in global exception filter가 자동으로 캐치
  • 적절한 사용자 친화적 응답 자동 전송

1.2 기본 동작

// HttpException 또는 그 서브클래스: 자동으로 적절한 응답 생성
// Unrecognized Exception (HttpException이 아닌 경우):
{
  "statusCode": 500,
  "message": "Internal server error"
}

 

중요 포인트:

  • http-errors 라이브러리를 부분적으로 지원
  • statusCode와 message 속성을 가진 예외는 자동으로 응답에 포함됨

2. HttpException 클래스

2.1 기본 사용법

import { HttpException, HttpStatus } from '@nestjs/common';

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

// 응답:
{
  "statusCode": 403,
  "message": "Forbidden"
}

2.2 생성자 시그니처

new HttpException(response, status, options?)

 

파라미터:

  1. response: string | object
    • string: 메시지만 오버라이드
    • object: 전체 응답 바디 커스터마이징
  2. status: HTTP 상태 코드
    • HttpStatus enum 사용 권장
  3. options (선택적):
    • cause: Error 객체 (로깅용, 응답에는 미포함)
    • description: 에러 설명

2.3 고급 사용 예시

전체 응답 바디 커스터마이징:

@Get()
async findAll() {
  try {
    await this.service.findAll()
  } catch (error) {
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error  // 내부 에러 추적용
    });
  }
}

// 응답:
{
  "status": 403,
  "error": "This is a custom message"
}

 

3. Exception Logging

3.1 기본 로깅 동작

// 로그에 출력되지 않는 예외들:
- HttpException (및 상속받은 클래스)
- WsException
- RpcException

// 모두 IntrinsicException을 상속받음
// 이유: 정상적인 애플리케이션 플로우의 일부로 간주

 

  • "왜 HttpException은 기본적으로 로깅되지 않나요?" → 정상적인 비즈니스 로직 흐름의 일부로 간주되기 때문

3.2 커스텀 로깅

로깅이 필요한 경우 커스텀 Exception Filter를 만들어야 함 (후술)


4. Custom Exceptions

4.1 예외 계층 구조 설계

// forbidden.exception.ts
export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

// 사용
@Get()
async findAll() {
  throw new ForbiddenException();
}

 

베스트 프랙티스:

  • HttpException을 상속받아 예외 계층 구조 생성
  • Built-in exception handler가 자동으로 인식
  • 타입 안정성 확보

4.2 Built-in HTTP Exceptions

NestJS가 제공하는 표준 예외들:

// 400번대
BadRequestException          // 400
UnauthorizedException        // 401
NotFoundException            // 404
ForbiddenException           // 403
NotAcceptableException       // 406
RequestTimeoutException      // 408
ConflictException            // 409
GoneException                // 410
PayloadTooLargeException     // 413
UnsupportedMediaTypeException // 415
UnprocessableEntityException // 422
ImATeapotException           // 418
MethodNotAllowedException    // 405
PreconditionFailedException  // 412

// 500번대
InternalServerErrorException // 500
NotImplementedException      // 501
BadGatewayException          // 502
ServiceUnavailableException  // 503
GatewayTimeoutException      // 504
HttpVersionNotSupportedException // 505

 

고급 사용:

throw new BadRequestException('Something bad happened', {
  cause: new Error(),
  description: 'Some error description',
});

// 응답:
{
  "message": "Something bad happened",
  "error": "Some error description",
  "statusCode": 400
}

 


5. Exception Filters 구현

5.1 기본 필터 구조

import { 
  ExceptionFilter, 
  Catch, 
  ArgumentsHost, 
  HttpException 
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

 

핵심 요소:

  1. @Catch(HttpException): 어떤 예외를 처리할지 지정
  2. ExceptionFilter 인터페이스 구현
  3. catch() 메서드 구현 필수

5.2 ArgumentsHost 이해

ArgumentsHost란?

  • 현재 실행 컨텍스트에 대한 강력한 유틸리티 객체
  • HTTP, WebSocket, Microservices 등 모든 컨텍스트에서 작동
  • Platform-agnostic 코드 작성 가능
const ctx = host.switchToHttp();        // HTTP 컨텍스트
const request = ctx.getRequest();       // Request 객체
const response = ctx.getResponse();     // Response 객체

// 또는
const wsCtx = host.switchToWs();        // WebSocket
const rpcCtx = host.switchToRpc();      // Microservices

 

 

  •  직접 Request/Response를 주입받지 않고 ArgumentsHost를 사용하나요?" → Platform-agnostic하게 만들어 HTTP 외에 WebSocket, Microservices에서도 동작하도록

6. Binding Filters (필터 적용 범위)

6.1 Method-scoped (메서드 레벨)

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

// 또는 클래스 전달 (DI 활용, 메모리 효율적)
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

인스턴스 vs 클래스:

  • 인스턴스: 즉시 생성, 더 많은 메모리 사용
  • 클래스: 프레임워크가 관리, DI 가능, 메모리 효율적 ✅

6.2 Controller-scoped (컨트롤러 레벨)

@Controller('cats')
@UseFilters(new HttpExceptionFilter())
export class CatsController {
  // 모든 라우트 핸들러에 적용
}

6.3 Global-scoped (전역)

방법 1: main.ts에서 등록

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

문제점:

  • 모듈 컨텍스트 외부에서 등록
  • DI(의존성 주입) 불가능

방법 2: 모듈에서 등록 (추천)

// app.module.ts
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

장점:

  • DI 가능 ✅
  • 다른 프로바이더 주입 가능
  • 진정한 의미의 global filter

고민해볼 사항

  • "전역 필터를 등록하는 두 가지 방법의 차이는?" → main.ts는 DI 불가, 모듈 등록은 DI 가능

7. Catch Everything Filter

7.1 모든 예외 캐치

@Catch()  // 파라미터 없음 = 모든 예외 캐치
export class CatchEverythingFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

7.2 HTTP Adapter 사용 이유

Platform-agnostic 코드:

// ❌ Platform-specific (Express)
response.status(status).json(body);

// ✅ Platform-agnostic (Express, Fastify 모두 동작)
httpAdapter.reply(response, body, status);

 

  • "HTTP Adapter를 사용하는 이유는?" → Express, Fastify 등 플랫폼 독립적인 코드 작성

7.3 필터 순서 주의사항

// ⚠️ 중요: Catch-all 필터를 먼저 선언
app.useGlobalFilters(new CatchEverythingFilter());
app.useGlobalFilters(new HttpExceptionFilter());

// 특정 타입 필터가 우선 처리되도록

 

// app.module.ts
// ✅ 최고의 방법: APP_FILTER + 명확한 @Catch
@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,  // @Catch(HttpException)
    },
    {
      provide: APP_FILTER,
      useClass: AllExceptionFilter,   // @Catch()
    },
  ],
})

export class AppModule {}

8. Filter Inheritance (상속)

8.1 BaseExceptionFilter 확장

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // 커스텀 로직 추가
    console.log('Exception caught:', exception);
    
    // 기본 동작 위임
    super.catch(exception, host);
  }
}

8.2 Global Filter에서 상속 사용

방법 1: HttpAdapter 주입

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const { httpAdapter } = app.get(HttpAdapterHost);
  
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
  await app.listen(3000);
}

방법 2: APP_FILTER 토큰 사용

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}

⚠️ 주의사항:

  • Method/Controller-scoped 필터는 new로 인스턴스화하지 말 것
  • 프레임워크가 자동으로 인스턴스화하도록 클래스만 전달

9. 리마인드

Q1: Exception Filter의 실행 순서는?

A:

  1. Method-scoped filter
  2. Controller-scoped filter
  3. Global filter
  4. Built-in global filter

더 구체적인 스코프의 필터가 먼저 실행되며, 예외가 처리되지 않으면 상위 스코프로 전파됩니다.

Q2: Filter에서 DI를 사용하려면?

A: 클래스를 전달하거나 APP_FILTER 토큰을 사용해야 합니다.

// ✅ DI 가능
@UseFilters(HttpExceptionFilter)

// ❌ DI 불가능
@UseFilters(new HttpExceptionFilter())

Q3: ArgumentsHost가 필요한 이유는?

A:

  • HTTP, WebSocket, Microservices 등 다양한 컨텍스트에서 동작하는 범용 필터 작성
  • Platform-agnostic 코드 구현
  • 실행 컨텍스트에 따라 적절한 객체 추출

Q4: IntrinsicException이란?

A: 정상적인 애플리케이션 플로우의 일부로 간주되는 예외들의 기반 클래스입니다.

  • HttpException
  • WsException
  • RpcException

이들은 기본적으로 로깅되지 않습니다.

Q5: Fastify에서 주의할 점은?

A:

// Express
response.json(body);

// Fastify
response.send(body);

// Platform-agnostic
httpAdapter.reply(response, body, status);

Q6: 실무에서 Exception Filter를 어떻게 활용하나요?

A:

  1. 로깅 및 모니터링: Sentry, DataDog 등과 통합
  2. 응답 포맷 통일: 회사 API 표준에 맞는 응답 구조
  3. 에러 추적: requestId, userId 등 컨텍스트 정보 추가
  4. 알림: 특정 에러 발생 시 Slack, Email 알림
@Catch()
export class SentryExceptionFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // Sentry에 에러 보고
    Sentry.captureException(exception);
    
    // 기본 처리
    super.catch(exception, host);
  }
}

 


10. 실전 예제

10.1 통합 에러 핸들링 시스템

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(
    private readonly httpAdapterHost: HttpAdapterHost,
    private readonly logger: Logger,
  ) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';
    let errorCode = 'INTERNAL_ERROR';

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const response = exception.getResponse();
      message = typeof response === 'string' 
        ? response 
        : (response as any).message;
      errorCode = (response as any).errorCode || 'HTTP_ERROR';
    }

    // 로깅
    this.logger.error({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(request),
      errorCode,
      message,
      stack: exception instanceof Error ? exception.stack : undefined,
      userId: request.user?.id,
      requestId: request.id,
    });

    // 응답
    const responseBody = {
      success: false,
      errorCode,
      message,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(request),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, status);
  }
}

 


핵심 요약

  1. Exception Layer: NestJS의 내장 예외 처리 시스템
  2. HttpException: 표준 HTTP 예외, 커스터마이징 가능
  3. Exception Filters: 예외 처리 로직 완전 제어
  4. Binding Scopes: Method → Controller → Global
  5. ArgumentsHost: Platform-agnostic 컨텍스트 접근
  6. DI: APP_FILTER 토큰으로 전역 필터에 DI 적용
  7. Inheritance: BaseExceptionFilter 확장으로 기본 동작 활용
  8. Catch Everything: @Catch() 파라미터 없이 모든 예외 처리

 

 


 

src/
├── common/
│   ├── exceptions/
│   │   ├── base/
│   │   │   ├── business.exception.ts           # 비즈니스 로직 예외 기본 클래스
│   │   │   └── domain.exception.ts             # 도메인 예외 기본 클래스
│   │   ├── domain/
│   │   │   ├── user.exception.ts               # 사용자 도메인 예외
│   │   │   ├── order.exception.ts              # 주문 도메인 예외
│   │   │   └── payment.exception.ts            # 결제 도메인 예외
│   │   └── index.ts
│   ├── filters/
│   │   ├── http-exception.filter.ts            # HTTP 예외 필터
│   │   ├── all-exception.filter.ts             # 전역 예외 필터
│   │   ├── validation-exception.filter.ts      # 검증 예외 필터
│   │   └── index.ts
│   ├── interfaces/
│   │   └── error-response.interface.ts         # 에러 응답 인터페이스
│   └── constants/
│       └── error-codes.constant.ts             # 에러 코드 상수
├── modules/
│   ├── users/
│   │   ├── users.controller.ts
│   │   ├── users.service.ts
│   │   └── dto/
│   └── orders/
│       ├── orders.controller.ts
│       └── orders.service.ts
└── app.module.ts

 

 

// src/common/constants/error-codes.constant.ts

export const ERROR_CODES = {
  // 공통 에러 (1000번대)
  INTERNAL_SERVER_ERROR: 'COMMON-1000',
  BAD_REQUEST: 'COMMON-1001',
  UNAUTHORIZED: 'COMMON-1002',
  FORBIDDEN: 'COMMON-1003',
  NOT_FOUND: 'COMMON-1004',
  VALIDATION_FAILED: 'COMMON-1005',

  // 사용자 도메인 (2000번대)
  USER_NOT_FOUND: 'USER-2000',
  USER_ALREADY_EXISTS: 'USER-2001',
  USER_INACTIVE: 'USER-2002',
  INVALID_CREDENTIALS: 'USER-2003',
  EMAIL_ALREADY_IN_USE: 'USER-2004',

  // 주문 도메인 (3000번대)
  ORDER_NOT_FOUND: 'ORDER-3000',
  ORDER_ALREADY_CANCELLED: 'ORDER-3001',
  ORDER_CANNOT_BE_MODIFIED: 'ORDER-3002',
  INSUFFICIENT_STOCK: 'ORDER-3003',

  // 결제 도메인 (4000번대)
  PAYMENT_FAILED: 'PAYMENT-4000',
  PAYMENT_ALREADY_PROCESSED: 'PAYMENT-4001',
  INVALID_PAYMENT_METHOD: 'PAYMENT-4002',
  INSUFFICIENT_BALANCE: 'PAYMENT-4003',

  // 외부 서비스 (5000번대)
  EXTERNAL_API_ERROR: 'EXTERNAL-5000',
  THIRD_PARTY_SERVICE_UNAVAILABLE: 'EXTERNAL-5001',
} as const;

export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];

 

 

// src/common/exceptions/base/business.exception.ts

import { HttpException, HttpStatus } from '@nestjs/common';
import { ErrorCode } from '../../constants/error-codes.constant';

export interface BusinessExceptionOptions {
  errorCode: ErrorCode;
  message: string;
  statusCode?: HttpStatus;
  details?: any;
  cause?: Error;
}

export class BusinessException extends HttpException {
  public readonly errorCode: ErrorCode;
  public readonly details?: any;
  public readonly timestamp: string;

  constructor(options: BusinessExceptionOptions) {
    const {
      errorCode,
      message,
      statusCode = HttpStatus.BAD_REQUEST,
      details,
      cause,
    } = options;

    super(
      {
        errorCode,
        message,
        details,
      },
      statusCode,
      { cause },
    );

    this.errorCode = errorCode;
    this.details = details;
    this.timestamp = new Date().toISOString();

    // 스택 트레이스 유지
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}
// src/common/filters/http-exception.filter.ts

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { ErrorResponse } from '../interfaces/error-response.interface';
import { BusinessException } from '../exceptions/base/business.exception';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    // BusinessException 처리
    if (exception instanceof BusinessException) {
      const errorResponse: ErrorResponse = {
        success: false,
        errorCode: exception.errorCode,
        message: exception.message,
        timestamp: exception.timestamp,
        path: request.url,
        method: request.method,
        statusCode: status,
        details: exception.details,
        traceId: this.getTraceId(request),
      };

      // 심각한 에러만 로깅 (4xx는 로깅 안함)
      if (status >= 500) {
        this.logger.error({
          ...errorResponse,
          stack: exception.stack,
          cause: exception.cause,
        });
      } else {
        this.logger.warn(errorResponse);
      }

      return response.status(status).json(errorResponse);
    }

    // 일반 HttpException 처리
    const message =
      typeof exceptionResponse === 'string'
        ? exceptionResponse
        : (exceptionResponse as any).message || exception.message;

    const errorResponse: ErrorResponse = {
      success: false,
      errorCode: this.getErrorCode(status),
      message: Array.isArray(message) ? message.join(', ') : message,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      statusCode: status,
      traceId: this.getTraceId(request),
    };

    this.logger.warn(errorResponse);

    response.status(status).json(errorResponse);
  }

  private getErrorCode(status: number): string {
    const errorCodeMap: Record<number, string> = {
      400: 'COMMON-1001',
      401: 'COMMON-1002',
      403: 'COMMON-1003',
      404: 'COMMON-1004',
      500: 'COMMON-1000',
    };

    return errorCodeMap[status] || `COMMON-${status}`;
  }

  private getTraceId(request: Request): string {
    // X-Request-ID 헤더 또는 자동 생성
    return (
      (request.headers['x-request-id'] as string) ||
      `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
    );
  }
}
// src/common/filters/all-exception.filter.ts

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { ErrorResponse } from '../interfaces/error-response.interface';
import { ERROR_CODES } from '../constants/error-codes.constant';

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionFilter.name);

  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();
    const request = httpAdapter.getRequestUrl(ctx.getRequest());

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';
    let errorCode = ERROR_CODES.INTERNAL_SERVER_ERROR;

    // HttpException 처리
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const response = exception.getResponse();
      message =
        typeof response === 'string'
          ? response
          : (response as any).message || exception.message;
    }
    // 알 수 없는 에러
    else if (exception instanceof Error) {
      message = exception.message;
      
      // 프로덕션에서는 상세 에러 숨김
      if (process.env.NODE_ENV === 'production') {
        message = 'An unexpected error occurred';
      }
    }

    const errorResponse: ErrorResponse = {
      success: false,
      errorCode,
      message,
      timestamp: new Date().toISOString(),
      path: request,
      statusCode: status,
      traceId: this.generateTraceId(),
    };

    // 모든 500 에러는 로깅 (알림 트리거 가능)
    if (status >= 500) {
      this.logger.error({
        ...errorResponse,
        stack: exception instanceof Error ? exception.stack : undefined,
        exception: exception,
      });

      // TODO: Sentry, DataDog 등으로 알림
      // this.notificationService.sendAlert(errorResponse);
    }

    httpAdapter.reply(ctx.getResponse(), errorResponse, status);
  }

  private generateTraceId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}

 

 

 

// src/modules/users/users.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  UseFilters,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { ValidationExceptionFilter } from '../../common/filters/validation-exception.filter';

@Controller('users')
// 검증 에러만 컨트롤러 레벨에서 처리
@UseFilters(ValidationExceptionFilter)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  async findOne(@Param('id') id: string) {
    // 서비스에서 예외 발생 → 자동으로 필터로 전달
    return await this.usersService.findOne(id);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() createUserDto: CreateUserDto) {
    // 검증 실패 시 ValidationExceptionFilter가 처리
    // 이메일 중복 시 EmailAlreadyInUseException 발생 → HttpExceptionFilter가 처리
    return await this.usersService.create(createUserDto);
  }

  @Post('login')
  @HttpCode(HttpStatus.OK)
  async login(
    @Body('email') email: string,
    @Body('password') password: string,
  ) {
    // InvalidCredentialsException 발생 가능
    const user = await this.usersService.validateCredentials(email, password);
    
    // TODO: JWT 토큰 생성
    return { message: 'Login successful', userId: user.id };
  }
}

 

 

// src/modules/users/dto/create-user.dto.ts

import { IsEmail, IsString, MinLength, MaxLength, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsEmail({}, { message: '올바른 이메일 형식이 아닙니다' })
  @IsNotEmpty({ message: '이메일은 필수입니다' })
  email: string;

  @IsString()
  @MinLength(8, { message: '비밀번호는 최소 8자 이상이어야 합니다' })
  @MaxLength(20, { message: '비밀번호는 최대 20자까지 가능합니다' })
  password: string;

  @IsString()
  @IsNotEmpty({ message: '이름은 필수입니다' })
  @MinLength(2, { message: '이름은 최소 2자 이상이어야 합니다' })
  @MaxLength(50, { message: '이름은 최대 50자까지 가능합니다' })
  name: string;
}

 

// src/common/interfaces/error-response.interface.ts

export interface ErrorResponse {
  success: false;
  errorCode: string;
  message: string;
  timestamp: string;
  path: string;
  method?: string;
  statusCode: number;
  details?: any;
  traceId?: string;
}

export interface ValidationErrorDetail {
  field: string;
  message: string;
  value?: any;
}

 

반응형
Comments