Nodejs

 

NestJS 기본 예외 및 반환 형식 정리

 

NestJS에서는 다양한 HTTP 예외를 제공하며, 예외 발생 시 특정한 형식의 JSON 응답을 반환합니다. 대표적인 예외와 그에 따른 반환 형식은 다음과 같습니다.

 

1. NotFoundException (404 Not Found)

{
    "response": {
        "message": "Resource not found",
        "error": "Not Found",
        "statusCode": 404
    },
    "status": 404,
    "options": {},
    "message": "Resource not found",
    "name": "NotFoundException"
}

 

2. BadRequestException (400 Bad Request)

{
    "response": {
        "message": "Invalid request parameters",
        "error": "Bad Request",
        "statusCode": 400
    },
    "status": 400,
    "options": {},
    "message": "Invalid request parameters",
    "name": "BadRequestException"
}

 

3. UnauthorizedException (401 Unauthorized)

{
    "response": {
        "message": "Unauthorized access",
        "error": "Unauthorized",
        "statusCode": 401
    },
    "status": 401,
    "options": {},
    "message": "Unauthorized access",
    "name": "UnauthorizedException"
}

 

 

4. ForbiddenException (403 Forbidden)

{
    "response": {
        "message": "Access to this resource is forbidden",
        "error": "Forbidden",
        "statusCode": 403
    },
    "status": 403,
    "options": {},
    "message": "Access to this resource is forbidden",
    "name": "ForbiddenException"
}

 

 

5. InternalServerErrorException (500 Internal Server Error)

{
    "response": {
        "message": "Internal server error",
        "error": "Internal Server Error",
        "statusCode": 500
    },
    "status": 500,
    "options": {},
    "message": "Internal server error",
    "name": "InternalServerErrorException"
}

 

 

6. ConflictException (409 Conflict)

{
    "response": {
        "message": "Conflict in the request",
        "error": "Conflict",
        "statusCode": 409
    },
    "status": 409,
    "options": {},
    "message": "Conflict in the request",
    "name": "ConflictException"
}

 

 

예외 핸들링 커스텀 방법

NestJS에서는 기본 제공되는 예외 외에도 HttpException을 확장하여 커스텀 예외를 만들 수도 있습니다.

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

export class CustomException extends HttpException {
  constructor() {
    super(
      {
        message: 'This is a custom exception',
        error: 'Custom Error',
        statusCode: 400,
      },
      HttpStatus.BAD_REQUEST,
    );
  }
}

이렇게 커스텀 예외를 정의하고 컨트롤러에서 throw new CustomException();을 호출하면 위에서 정의한 JSON 응답을 반환할 수 있습니다.

이 외에도 ExceptionFilter를 사용하여 예외를 전역적으로 처리할 수도 있습니다.

 

 

 

 

 

★ 직접 예외 반환 형식 커스텀하기

NestJS에서 커스텀 예외 필터를 사용하면, 응답 형식을 원하는 대로 변경할 수도 있습니다.

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

@Catch(HttpException)
export class CustomExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    console.error('필터에서 예외 발생 =================================');

    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    let errorMessage: string | string[] = 'An error occurred';
    let errorData: Record<string, any> = {};
    let errorType: string = 'Error';

    if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
      errorData = exceptionResponse as Record<string, any>;

      if ('message' in errorData) {
        errorMessage = errorData.message as string | string[];
      }

      if ('error' in errorData) {
        errorType = errorData.error as string;
      }
    } else if (typeof exceptionResponse === 'string') {
      errorMessage = exceptionResponse;
      errorType = exception.name;
    }

    response.status(status).json({
      success: false,
      statusCode: status,
      error: errorType,
      message: errorMessage,
      timestamp: new Date().toISOString(),
    });
  }
}

 

이 필터를 main.ts에 등록하면 적용됩니다.

 

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from 'common/custom-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new CustomExceptionFilter());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap().catch((err) => {
  console.error('Bootstrap error:', err);
  process.exit(1);
});

 

이제 예외 발생 시 다음과 같은 커스텀 응답이 반환됩니다.

{
    "success": false,
    "statusCode": 404,
    "error": "Not Found",
    "message": "Movie with ID 11 not found",
    "timestamp": "2025-02-24T17:07:14.604Z"
}

 

 

샘플 컨트롤

import {
  Controller,
  Delete,
  Get,
  Patch,
  Post,
  Body,
  Param,
  HttpCode,
  NotFoundException,
  HttpStatus,
} from '@nestjs/common';
import { AppService } from './app.service';

interface Movie {
  id: number;
  title: string;
  name: string;
  character: string[];
}

@Controller('movie')
export class AppController {
  private movies: Movie[] = [
    {
      id: 1,
      title: 'The Shawshank Redemption',
      name: '스크린',
      character: ['스크', '스 '],
    },
    {
      id: 2,
      title: 'The Godfather',
      name: '미디안',
      character: ['미디안', 'AA'],
    },
    {
      id: 3,
      title: 'Pulp Fiction',
      name: '레나',
      character: ['레나', '로버트 T. 로스'],
    },
    {
      id: 4,
      title: 'The Dark Knight',
      name: '블 Panther',
      character: ['블11 Panther', 'aa'],
    },
  ];

  constructor(private readonly appService: AppService) {}

  @Get()
  getMovies() {
    console.log('Fetching all movies...');
    return this.movies;
  }

  @Get(':id')
  getMovie(@Param('id') id: string) {
    console.log(`Fetching movie with ID: ${id}`);
    const movie = this.movies.find((m) => m.id === +id);
    if (!movie) {
      throw new NotFoundException(`Movie with ID ${id} not found`);
    }
    return movie;
  }

  @Post()
  @HttpCode(HttpStatus.CREATED) // 201 상태 코드 반환
  postMovie(@Body() body: { name: string; character: string[] }) {
    console.log(`Adding new movie: ${body.name}`);
    return {
      id: Math.floor(Math.random() * 1000),
      ...body,
    };
  }

  @Patch(':id')
  patchMovie(
    @Param('id') id: string,
    @Body() body: { name?: string; character?: string[] },
  ) {
    console.log(`Updating movie with ID: ${id}`);
    return {
      id: Number(id),
      name: body.name || '어벤져스',
      character: body.character || ['아이언맨', '블랙위도우'],
    };
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT) // 204 상태 코드 반환
  deleteMovie(@Param('id') id: string) {
    console.log(`Deleting movie with ID: ${id}`);
    return;
  }
}

 

결론

  • NestJS의 기본 예외 반환 형식은 response, status, message, name 등의 공통 구조를 가짐.
  •  
  • @nestjs/common 모듈에 내장된 예외 클래스들을 활용하면 자동으로 해당 형식으로 응답.
  •  
  • 커스텀 예외 필터를 사용하면 원하는 형태로 반환 형식을 변경 가능.

 

 

 

 

 

1.★NestJs 프로젝트 내 전체  공통 API 응답형식 설정★ 

 

 

일관된 API 응답 구조의 필요성

  1. 일관성 유지 → 성공과 실패의 응답 구조가 다르면 클라이언트에서 처리 로직이 복잡해짐
  2. 확장성 향상 → 특정 필드를 추가할 때, 기존 API 사용 방식에 영향을 주지 않음
  3. 디버깅 용이 → 모든 응답을 같은 형식으로 관리하면 로그 분석과 디버깅이 쉬워짐

 

권장하는 API 응답 형식

NestJS에서는 일반적으로 다음과 같은 통일된 응답 형식을 사용하면 좋습니다.

{
  "status": 200, // HTTP 상태 코드
  "success": true, // 요청 성공 여부
  "message": "Request successful", // 응답 메시지
  "data": { ... }, // 실제 데이터 (성공 시)
  "error": null // 실패 시 에러 정보 (성공 시에는 null)
}
{
  "status": 400, // HTTP 상태 코드
  "success": false, // 요청 성공 여부
  "message": "Invalid request parameters", // 에러 메시지
  "data": null, // 성공 시에는 데이터, 실패 시에는 null
  "error": {
    "code": "BadRequestException",
    "message": "The provided parameters are incorrect."
  }
}

 

NestJS에서 일관된 응답 처리하기

1️⃣ 응답 형식을 관리하는 DTO (Data Transfer Object) 생성

NestJS에서는 DTO(Data Transfer Object) 를 활용하여 응답을 정의할 수 있습니다.

 response.dto.ts (응답 DTO 생성)

export class ApiResponse<T> {
  status: number;
  success: boolean;
  message: string;
  data: T | null;
  error: { code: string; message: string } | null;

  constructor(
    status: number,
    success: boolean,
    message: string,
    data: T | null = null,
    error: { code: string; message: string } | null = null
  ) {
    this.status = status;
    this.success = success;
    this.message = message;
    this.data = data;
    this.error = error;
  }
}

 

2️⃣ 컨트롤러에서 API 응답 적용

users.controller.ts

import { Controller, Get, Post, Body, Param, HttpStatus } from '@nestjs/common';
import { ApiResponse } from './response.dto';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    const user = await this.userService.findUserById(id);
    if (!user) {
      return new ApiResponse(
        HttpStatus.NOT_FOUND,
        false,
        'User not found',
        null,
        { code: 'NotFoundException', message: `User with ID ${id} not found` }
      );
    }

    return new ApiResponse(HttpStatus.OK, true, 'User retrieved successfully', user);
  }

  @Post()
  async createUser(@Body() createUserDto: any) {
    try {
      const newUser = await this.userService.createUser(createUserDto);
      return new ApiResponse(HttpStatus.CREATED, true, 'User created successfully', newUser);
    } catch (error) {
      return new ApiResponse(
        HttpStatus.BAD_REQUEST,
        false,
        'User creation failed',
        null,
        { code: 'BadRequestException', message: error.message }
      );
    }
  }
}

 

3️⃣ 전역 예외 필터를 사용하여 예외도 동일한 형식으로 반환

모든 예외를 통일된 응답 형식으로 처리하려면 전역 예외 필터를 사용하는 것이 좋습니다.

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { Response } from 'express';
import { ApiResponse } from './response.dto';

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

    response.status(status).json(
      new ApiResponse(
        status,
        false,
        exceptionResponse['message'] || 'An error occurred',
        null,
        {
          code: exception.name || 'HttpException',
          message: exceptionResponse['message'] || 'No additional details',
        }
      )
    );
  }
}

 

이 필터를 전역으로 적용 (main.ts)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './filters/exception.filter';

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

 

 

결과: 성공과 실패 응답이 일관된 형태로 반환

✅ 성공 시

{
  "status": 200,
  "success": true,
  "message": "User retrieved successfully",
  "data": {
    "id": "123",
    "username": "john_doe",
    "email": "john@example.com"
  },
  "error": null
}

❌ 실패 시 (예: 사용자를 찾을 수 없음)

{
  "status": 404,
  "success": false,
  "message": "User not found",
  "data": null,
  "error": {
    "code": "NotFoundException",
    "message": "User with ID 123 not found"
  }
}

 

결론

  1. 성공 & 실패 응답을 동일한 형식으로 반환하는 것이 좋음
  2. DTO(ApiResponse<T>)를 만들어서 응답 구조를 강제하면 가독성과 유지보수성이 향상됨
  3. 전역 예외 필터(GlobalExceptionFilter)를 사용하면 모든 예외가 일관된 JSON 형식으로 반환
  4. NestJS 프로젝트에서 이런 방식으로 API 응답을 통일하는 것이 일반적인 좋은 관행

 

 

 

Express

// 공통 API 응답 클래스
class ApiResponse {
    constructor(status,success, message, data = null,error = null) {
      // 201 OK, 400 Bad Request status code로 설정
      if(!status){
        if(success==true)this.status = 201;
        else if(success = false) this.status = 400;
      }else this.status = status;
        
      this.success = success; // 성공 여부 (true / false)
      this.message = message; // 응답 메시지
      this.data = data; // 응답 데이터 (옵션)
      this.error = error; // 응답 데이터 (옵션)
    }
}


module.exports = {
    ApiResponse,
} 

 

Express 사용 예

// ✅ 1. 성공 응답 - 사용자 조회
console.log(new ApiResponse(200, true, "사용자 정보를 성공적으로 가져왔습니다.", { id: "123", usercode: "john_doe", email: "john@example.com" }));

// ✅ 2. 성공 응답 - 목록 조회 (배열)
console.log(new ApiResponse(200, true, "상품 목록을 성공적으로 불러왔습니다.", [{ id: "1", code: "노트북" }, { id: "2", code: "스마트폰" }]));

// ✅ 3. 성공 응답 - 생성 완료 (201 Created)
console.log(new ApiResponse(201, true, "사용자가 성공적으로 생성되었습니다.", { id: "124", usercode: "jane_doe", email: "jane@example.com" }));

// ✅ 4. 성공 응답 - 업데이트 완료
console.log(new ApiResponse(200, true, "사용자 정보가 성공적으로 업데이트되었습니다.", { id: "123", usercode: "john_doe_updated" }));

// ✅ 5. 성공 응답 - 삭제 완료 (204 No Content)
console.log(new ApiResponse(204, true, "사용자가 성공적으로 삭제되었습니다."));

// ✅ 6. 오류 응답 - 잘못된 요청 (400 Bad Request)
console.log(new ApiResponse(400, false, "잘못된 요청입니다.", null, { code: "ValidationError", message: "사용자 이름은 필수 입력 항목입니다." }));

// ✅ 7. 오류 응답 - 인증 실패 (401 Unauthorized)
console.log(new ApiResponse(401, false, "인증이 필요합니다.", null, { code: "AuthError", message: "토큰이 없거나 유효하지 않습니다." }));

// ✅ 8. 오류 응답 - 권한 없음 (403 Forbidden)
console.log(new ApiResponse(403, false, "접근 권한이 없습니다.", null, { code: "PermissionError", message: "이 리소스에 대한 접근 권한이 없습니다." }));

// ✅ 9. 오류 응답 - 찾을 수 없음 (404 Not Found)
console.log(new ApiResponse(404, false, "요청한 정보를 찾을 수 없습니다.", null, { code: "NotFoundError", message: "해당 ID에 해당하는 사용자가 존재하지 않습니다." }));

// ✅ 10. 오류 응답 - 서버 오류 (500 Internal Server Error)
console.log(new ApiResponse(500, false, "서버 내부 오류가 발생했습니다.", null, { code: "ServerError", message: "예기치 않은 서버 오류가 발생했습니다." }));

 

 

 

 

 

 

 

 

2.★스프링부트에서 반환 형식

// 공통 응답 DTO (Spring Boot)
public class ApiResponse<T> {
    private int status;
    private boolean success;
    private String message;
    private T data;
    private ApiError error;

    public ApiResponse(int status, boolean success, String message, T data, ApiError error) {
        this.status = status;
        this.success = success;
        this.message = message;
        this.data = data;
        this.error = error;
    }

    public static <T> ApiResponse<T> success(T data, String message) {
        return new ApiResponse<>(200, true, message, data, null);
    }

    public static <T> ApiResponse<T> error(int status, String message, String errorCode, String errorMessage) {
        return new ApiResponse<>(status, false, message, null, new ApiError(errorCode, errorMessage));
    }

    // Getter & Setter 생략
}

class ApiError {
    private String code;
    private String message;

    public ApiError(String code, String message) {
        this.code= code;
        this.message= message;
    }
    // Getter & Setter 생략
}

// 전역 예외 처리 (Spring Boot)
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error(500, "Internal Server Error", ex.getClass().getSimpleName(), ex.getMessage()));
    }
}

 

 

 

 

 

 

3.★Django REST Framework(DRF) 응답 형식 설정

  • ApiResponse와 ApiError를 DRF의 Serializer로 구현
  • 성공 및 오류 응답을 위한 Response 유틸리티 함수 생성
  • 전역 예외 처리기(Custom Exception Handler) 등록

 

Django REST Framework(DRF) 응답 형식 설정

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import exception_handler
from rest_framework.serializers import Serializer, CharField, BooleanField, IntegerField


# ✅ ApiError Serializer
class ApiErrorSerializer(Serializer):
    code = CharField()
    message = CharField()


# ✅ 공통 응답 Serializer
class ApiResponseSerializer(Serializer):
    status = IntegerField()
    success = BooleanField()
    message = CharField()
    data = CharField(allow_null=True)
    error = ApiErrorSerializer(allow_null=True)


# ✅ 성공 응답 생성 함수
def success_response(data=None, message="Success"):
    return Response({
        "status": 200,
        "success": True,
        "message": message,
        "data": data,
        "error": None
    }, status=status.HTTP_200_OK)


# ✅ 오류 응답 생성 함수
def error_response(status_code, message, error_code, error_message):
    return Response({
        "status": status_code,
        "success": False,
        "message": message,
        "data": None,
        "error": {
            "code": error_code,
            "message": error_message
        }
    }, status=status_code)


# ✅ 전역 예외 처리기 (DRF Custom Exception Handler)
def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)

    if response is None:
        return error_response(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            message="Internal Server Error",
            error_code=exc.__class__.__name__,
            error_message=str(exc)
        )

    return error_response(
        status_code=response.status_code,
        message=response.data.get('detail', 'Error Occurred'),
        error_code=exc.__class__.__name__,
        error_message=str(exc)
    )

 

적용 방법

  1. success_response(data, message) → 성공 응답을 반환할 때 사용
  2. error_response(status, message, error_name, details) → 오류 응답을 반환할 때 사용
  3. custom_exception_handler → DRF의 EXCEPTION_HANDLER에 등록

 

Django settings.py에 Custom Exception Handler 설정 추가

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler'
}

 

 

 

실제 프로젝트에서 사용 예시

사용 예: DRF View에서 공통 응답 사용하기

from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from myapp.utils import success_response, error_response

class UserDetailView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        try:
            user_data = {
                "id": str(request.user.id),
                "username": request.user.username,
                "email": request.user.email
            }
            return success_response(data=user_data, message="User retrieved successfully")
        
        except Exception as e:
            return error_response(500, "Failed to retrieve user", e.__class__.__name__, str(e))

 

 

 

 

 

about author

PHRASE

Level 60  라이트

백성을 잘 다스리려면 백성으로 하여 수치심이 있음을 알려야 할 것이다. -관자

댓글 ( 0)

댓글 남기기

작성