Skip to main content

Command Palette

Search for a command to run...

[ 살펴보기 ] NestJS - 예외필터와 인터셉터

Published
4 min readView as Markdown
[ 살펴보기 ] NestJS - 예외필터와 인터셉터
C

A developer living in Busan, Korea

NestJS는 error가 발생 했을 때 해당 에러에 대한 예외 처리를 application code가 하고 있지 않다면 default로 제공되는 예외 필터가 발생한 error에 대한 예외 처리를 한다. Default 예외 필터가 client에게 return하는 정보는 다음과 같다.

{
  "statusCode": 500,
  "message": "Internal server error"
}

Default 예외 필터외에 직접 예외 필터를 정의해 적용할 수도 있다. 예를 들어 발생하는 모든 예외 정보를 log 파일로 남기거나 응답 객체를 변경하거나 등의 작업을 custom 예외 필터를 적용해 한 곳에서 처리할 수 있다.

다음은 발생한 예외정보를 log하고 response 객체를 변경하는 예제이다. ( 아래서 사용하는 logger는 Logger 포스트에서 살펴보았던 winston logger를 계속해서 적용하고 있다 )

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

type ExceptionResponse = {
  message: string;
  error: string;
  statusCode: number;
};

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

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

    this.logger.error(exceptionRes.message);

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

위의 예제에서 볼 수 있듯이 예외 필터는 ExceptionFilter interface를 구현해야 한다. 그리고 Catch decorator에 HttpException class를 전달해 해당 filter가 http 관련 error만 catch할 수 있도록 한다.

이제 위에서 정의한 Exception filter를 controller scope로 적용해보자. filter를 적용할 때는 UseFilter decorator를 사용한다.

...
import { UseFilters, BadRequestException } from '@nestjs/common';
import { HttpExceptionFilter } from 'src/filter/http-exception.filter';

@Controller('users')
@UseFilters(HttpExceptionFilter)
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
  ) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    throw new BadRequestException('this is a bad reqeust');
  }

}

위의 예제에서 /users post method 요청이 들어오면 NestJS에서 제공하는 예외 class중 하나인 BadRequestException을 통해 예외를 발생시키고 있다.

users controller에 우리가 정의한 예외 필터가 적용되어 있으므로 create method에서 발생한 예외는 filter에서 catch되고 response도 filter에서 정의한 형태로 client에게 전달된다.

예외필터 역시 global scope로 적용할 수 있다. 만약 예외 필터에 아무런 dependency를 inject하고 있지 않다면 다음과 같이 main.ts를 통해 적용할 수 있다.

...
import { HttpExceptionFilter } from 'src/filter/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger({
      ...
    }),
  });
  ...
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(4000);
}
bootstrap();

하지만 위의 예제에서 우리가 사용하는 예외필터에 logger를 inject하고 있으므로 global scope로 적용할 때 main.ts에서 적용하는 것이 아닌 module의 provider로 제공해야한다. 아래의 예제에서는 app module에 예외 필터를 제공하고 있다.

import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from 'src/filter/http-exception.filter';

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

Interceptor

Interceptor를 통해 request가 handler로 전달되기 전과 handler에 의해 request가 처리되어 response가 전달되기 전 두 시점 모두에서 원하는 작업을 할 수 있다. 예외 필터가 발생한 예외에 대한 response를 가로채서 최종 client에 전달되는 response를 변경했듯이 Interceptor 또한 request와 response를 중간에서 가로채서 필요한 변경을 추가할 수 있다.

다음은 request가 발생 했을 때 request url을 log하고 request 처리가 완료되고 response가 return되었을 때 request 처리 시간을 log하는 Interceptor의 예제다.

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private readonly logger: Logger) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    this.logger.log(`Interceptor - Request Log : ${request.url}`);

    const now = Date.now();
    return next.handle().pipe(
      tap(() => {
        this.logger.log(
          `Interceptor - Response Log : ${request.url} took : ${Date.now() - now}ms`,
        );
      }),
    );
  }
}

Interceptor class는 Nest Interceptor interface를 구현해야 한다. 그리고 intercept method의 두 번째 인자에서 구현하고 있는 handle method는 Observable 객체를 return하며 이를 rxJS method를 통해 response 객체에 추가 작업을 할 수 있다.

위에서 정의한 interceptor를 controller에 적용시켜 보자.

import { UseInterceptors } from '@nestjs/common';

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
  ) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
  ...
}

Interceptor를 적용할 때는 UseInterceptor decorate를 통해 적용할 수 있다. 위의 예제처럼 controller scope로 적용하면 해당 controller에 소속된 모든 route handler에 interceptor가 적용된다.

Interceptor 역시 global scope로 적용할 수 있다. 위의 예제에서는 logger를 inject 하고 있으므로 main.ts가 아닌 module의 provider로 제공하여 globals scope로 적용한다. 아래의 예제에선 app module을 통해 interceptor를 제공하고 있다.

...

import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from 'src/interceptor/loggin.interceptor';

@Module({
  ...
  providers: [
    Logger,
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

만약 interceptor를 통해 response 형태를 변경하고 싶다면 다음과 같이 처리할 수 있다.

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        return { success: true, data };
      }),
    );
  }
}

위의 interceptor를 적용하고 client 요청의 response를 확인해보면 interceptor에서 설정한 형태로 response가 전달된 것을 확인할 수 있다.

More from this blog

[ 살펴보기 ] TypeORM - Transactions, Migration

Transation Database 종류에 따라 detail한 부분은 차이점이 조금씩 있겠지만 각 sql statement는 개별적인 transaction block을 통해 실행되며 Database 설정에 따라 sql statement의 실행 결과가 자동으로 commit되어 영구히 적용되거나 commit을 직접 실행하기 전까지는 영구히 적용되지 않을 수 있다. 대부분의 경우 default로 sql statement 실행 결과가 자동으로 comm...

Feb 9, 20256 min read
[ 살펴보기 ] TypeORM - Transactions, Migration

Dev Diary

184 posts