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 응답 구조의 필요성
- 일관성 유지 → 성공과 실패의 응답 구조가 다르면 클라이언트에서 처리 로직이 복잡해짐
- 확장성 향상 → 특정 필드를 추가할 때, 기존 API 사용 방식에 영향을 주지 않음
- 디버깅 용이 → 모든 응답을 같은 형식으로 관리하면 로그 분석과 디버깅이 쉬워짐
✅ 권장하는 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"
}
}
✅ 결론
- 성공 & 실패 응답을 동일한 형식으로 반환하는 것이 좋음
- DTO(ApiResponse<T>)를 만들어서 응답 구조를 강제하면 가독성과 유지보수성이 향상됨
- 전역 예외 필터(GlobalExceptionFilter)를 사용하면 모든 예외가 일관된 JSON 형식으로 반환됨
- 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)
)
적용 방법
- success_response(data, message) → 성공 응답을 반환할 때 사용
- error_response(status, message, error_name, details) → 오류 응답을 반환할 때 사용
- 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))














댓글 ( 0)
댓글 남기기