[NestJS 공식문서 정독하기] Overview - Pipes

2022. 8. 2. 14:26Web/NestJS

반응형
🔗  https://docs.nestjs.com/pipes
 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

  • pipe는 PipeTransform 인터페이스를 구현하며 @Injectable() 데코레이터가 적용되어 있는 클래스임
  • pipe는 두 가지 보편적인 용도가 있음
    • transformation: input 데이터를 적절한 형태로 변환
    • validation: input 데이터를 검증하고 유효하지 않으면 예외를 던짐
  • pipe는 controller route handler의 매개변수에 대해 동작함
  • controller의 메소드가 호출되기 직전에 pipe가 동작함
  • pipe는 exceptions zone에서 동작함 (pipe 동작 중 예외를 던지면 exceptions layer에서 exception filter가 해당 예외를 처리하고 controller의 메소드는 호출되지 않음)
  • 내장 pipe 목록

Binding pipes

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}
@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}
  • 특정 route handler 메소드에 연결하고 싶은 경우 위와 같이 적용할 수 있음
  • 클래스 자체를 전달해서 NestJS의 DI를 활용할 수 있음
  • pipe의 옵션을 커스터마이징해서 사용하고 싶으면 인스턴스를 전달할 수도 있음

Custom pipes

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}
  • PipeTransform<T, R> 는 모든 pipe가 구현해야하는 인터페이스임 (T: value의 타입, R: transform의 반환 타입)
  • transform의 value 는 처리하는 파라미터의 값이고, metadata 는 처리하는 파라미터의 메타데이터임
export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}
  • type: 전달 받은 매개변수가 @Body() , @Query() , @Param() , 혹은 커스텀 파라미터인지 구분함
  • metatype: 매개변수의 메타타입 (route handler에서 타입이 명시되지 않았으면 undefined, 타입이 인터페이스일 경우 트랜스파일 과정에서 타입이 사라지므로 그냥 Object로 나옴)
  • data: 데코레이터에 전달된 문자열 값 (전달된 값이 없으면 undefined)

Schema based validation

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}
  • 위와 같이 DTO로 요청을 받을 때 클래스 필드들을 검증하고 싶음
  • route handler 메소드 내부에서 검증을 할 수도 있지만 SRP를 위반하는 행위임
  • 검증 클래스를 만들어서 검증을 위임할 수도 있지만 route handler 메소드에서 매번 잊지 않고 호출해줘야 함
  • 검증 미들웨어? 안됨. 미들웨어는 generic 처리가 안되고 execution context를 몰라서 handler와 파라미터들을 알 수 없음
  • 따라서, pipe를 활용해야 함

Object schema validation

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value);
    if (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}
  • Joi 라이브러리 사용
  • 생성자로 schema 를 받고 schema.validate 를 호출해서 검증

Binding validation pipes

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
  • @UsePipes() 를 사용해서 메소드 레벨에 pipe를 바인딩 할 수 있음
  • 이전에 생성한 JoiValidationPipe 의 생성자로 schema를 전달해서 인스턴스를 생성하고 데코레이터에 전달함

Class validator

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToInstance(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}
@Post()
async create(
  @Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
  this.catsService.create(createCatDto);
}
  • transform이 async 함수임 (NestJS는 async/sync pipe를 모두 지원함)
  • plainToInstance 메소드로 일반 객체를 검증이 가능한 클래스로 변환함 (일반 객체 요청에는 타입 정보와 데코레이터가 없기 때문)

Global scoped pipes

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
  • ValidationPipe 가 모든 route handler에 적용되도록 global-scoped로 만들 수 있음
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}
  • useGlobalPipes 메소드를 사용한 글로벌 pipe 설정은 DI를 활용할 수 없음
  • 해당 이슈를 해결하기 위해 아무 모듈에서 글로벌 pipe를 적용할 수 있음
  • 어떤 모듈에서 설정해도 global-scope가 되기 때문에 ValidationPipe 가 정의된 모듈에서 적용하느 것을 추천함

The built-in ValidationPipe

  • 내장된 ValidationPipe 가 존재함
  • 위에서 구현한 클래스보다 추가적인 옵션들을 포함하고 있음
  • 여기에서 자세한 내용 확인

Transformation use case

  • pipe에서 validation 뿐만 아니라 transform도 가능함 (PipeTransform의 transform 함수의 반환값이 기존 값을 완전히 덮어쓰기 때문)
  • handler에 도달하기 전에 특정 타입으로 변환이 되어야 하거나, 특정 필드가 없을 때 기본값을 추가해서 전달하는 과정이 필요할 때 유용함
반응형