[NestJS docs] Pipes

nakkim·2022년 7월 18일
0

NestJS docs

목록 보기
4/10
post-custom-banner

https://docs.nestjs.com/pipes

Pipes

파이프는 PipeTransform 인터페이스를 구현, @Injectable() 데코레이터를 사용한다.

일반적인 파이프 이용 사례:

  • transformation: 입력 데이터를 원하는 형식으로 바꿈(string to number)
  • validation: 입력 데이터를 검증
    • 유효하면 그대로 넘겨줌
    • 유효하지 않으면 예외를 던짐

두 경우 모두 (컨트롤러 라우트 핸들러에 의해 처리되는) 인수에 대해 동작

  1. Nest는 메서드가 호출되기 직전에 파이프를 삽입
  2. 파이프는 메서드로 넘어갈 인수를 수신하여 이들에 대해 동작

모든 transformation이나 validation은 라우트 핸들러가 호출된 후에 수행된다.

(라우트 핸들러 호출 → 파이프 → 메서드 호출?)

파이프는 빌트인도 많고, 만들어서 쓸 수도 있음

이번 챕터에서는 여러 빌트인 파이프를 소개하고 그들을 라우트 핸들러에 바인드 하는 방법, 그리고 커스텀 파이프를 만들어서 파이프를 구축하는 방법을 알려줌

파이프는 exception zone에서 실행된다. 이것은 파이프가 예외를 던지는 경우 exception layer에서 처리한다는 것을 뜻함. 그래서, 파이프가 예외를 던지고 나면, 컨트롤러 메서드는 실행되지 않음.

Built-in pipes

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

ParseIntPipe

이 파이프는 transformation의 예시이다.

데이터를 integer로 바꿔줌

Binding pipes

파이프를 사용하기 위해서, 파이프 클래스의 인스턴스를 적절한 컨텍스트에 바인드 해야함

ParseIntPipe를 예시로 보자. (예시에서는 클래스를 넘겨줘서 프레임워크에 DI 책임을 넘김)

우리는 id 파라미터를 받아서 그걸 숫자로 바꾸고 싶음

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

만약

GET localhost:3000/abc

이렇게 호출되면, abc를 숫자로 바꿀 수 없기 때문에 아래의 예외를 받음

{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

파이프 클래스 대신 인스턴스를 전달하는 것은 옵션을 넘겨줌으로써 파이프의 동작을 커스텀하려고 할 때 유용하다.

@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}

Custom pipes

ParseIntPipeValidationPipe를 직접 구현하면서 파이프가 어떻게 생겨먹었는지 알아보자.

Validation pipes

일단은 입력값을 받아서 바로 리턴하는 파이프를 만들자.

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

@Injectable()
export class ValidationPipe implements PipeTransform {
	transform(value: any, metadata: ArgumentMetadata) {
		return value;
	}
}

PipeTransform<T, R>은 어느 파이프에서나 구현해야 하는 제너릭 인터페이스이다. T는 transform() 메서드의 입력값의 타입을 나타내고, R은 리턴값의 타입을 나타낸다.

모든 파이프는 transform(value, metadata) 메서드를 구현해야 함

  • value: 처리될 데이터
  • metadata: 처리될 데이터의 메타데이터

아래는 ArgumentMetadata 인터페이스를 나타냄

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}
  • type: value가 어디서 온 데이터인지
  • metatype: 타입을 알려줌(String인지 뭐..)
  • 데코레이터에 패스된 문자열
    • 예: @Body(’string’)에서 string

Transformation pipes

transformation 파이프는 클라이언트 요청과 리퀘스트 핸들러 사이에서 데이터를 변환해준다.

아래는 간단한 ParseIntPipe이다. Nest가 잘 만들어놨기 ㄸ문에, 간단한 예제로써만 보도록 하자.

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

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

또다른 유용한 예시: DB에서 유저를 고를 때 사용할 수 있다. (id를 받아서 유저 데이터 리턴)

@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  return userEntity;
}

Schema based validation

CatsController의 create() 메서드에서 매개변수를 받을 때, CreateCatDto 타입을 받는다.

CreateCatDto를 한번 보자.

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

우리는 body에 포함된 데이터의 형식이 CreateCatDto에 맞는지 확인하고싶다.

이 검증 과정을 라우트 핸들러 메서드 안에서 할 수도 있지만, 그건 SRP(single responsibility principle)에 어긋남

그러면 validator class를 만들어서 위 검증 과정을 시키는 방법도 있음. 하지만 이거는 각 메서드 시작 전에 이 validator를 호출해야 된다는 걸 기억하고 있어야 한다는 단점이 있음

그럼 validation 미들웨어를 만들자? 좋아보이지만, 프로그램 전체를 가로 질러서 사용할 수 있는 generic middleware를 만드는 건 불가능해.. 왜냐하면 미들웨어가 호출된 핸들러와 해당 매개변수를 포함하여 실행 컨텍스트를 알지 못하기 때문임

그래서.

파이프를 쓴다~

Object schema validation

There are several approaches available for doing object validation in a clean, DRY way. One common approach is to use schema-based
validation. Let's go ahead and try that approach.

Joi 라이브러리를 사용하면 간단하게 스키마를 만들 수 있음

Joi 기반 스키마 검증 파이프를 만들어보자

$ npm install --save joi
$ npm install --save-dev @types/joi

아래 예제는 스키마를 생성자 인수로 사용하는 간단한 클래스를 만든다.

그 다음 schema.validate() 메서드를 사용해서 받은 스키마를 검증한다.

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;
  }
}

Binding validation pipes

우리는 위의 Binding pipes에서 transformation 파이프를 바인드하는 방법을 알아봤다.

validation 파이프를 바인딩하는 것도 쉬움

일단 우리는 메서드 콜 레벨에서 파이프를 바인드하고 싶음

  1. JoiValidationPipe의 인스턴스 생성
  2. 파이프 클래스 생성자에 적절한 스키마 전달
  3. 파이프를 메서드에 바인드

이것을 @UsePipes() 데코레이터를 이용해서 할 거임

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
	this.catsService.create(createCatDto);
}

Class validator

(TS 꼭 필요!)

$ npm i --save class-validator class-trnasformer

Nest works well with the class-validator library.

이 라이브러리는 데코레이터 기반 검증을 가능하게 해준다. 데코레이터 기반 검증이란 Nest의 파이프와 합쳐졌을 때 유용하다. 왜냐. 처리되는 데이터의 metatype에 접근할 수 있기 때문.

우리는 CreateCatDto에 데코레이터를 추가할 수 있다.

아래는 예시

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

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

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

Read more about the class-validator decorators

이제 우리는 위의 데코레이터들을 이용해서 ValidationPipe를 만들 수 있다.

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);
  }
}

천천히 위 코드를 살펴보자.

  1. async가 있는 transform() 메서드를 봐라

    Nest가 동기/비동기 파이프를 둘 다 지원하기 때문에 가능

    class-validator 검증의 일부가 비동기적일 수 있기 때문에 async 사용

  2. toValidate()

    현재 인수가 네이티브 자바스크립트 타입이라면(타입스크립트가 아니라면) 검증을 우회시킴 - 검증 데코레이터를 가질 수 없기 때문에

  3. plainToInstance()

    검증을 위해, 자바스크립트 객체를 typed 객체로 변환

    이걸 하는 이유: post body 객체가 네트워크 요청에서 deserialize 될 때, 타입 정보가 없기 때문

    Class-validator는 검증 데코레이터를 사용하기 때문에 들어온 바디를 적절하게 decorate된 객체로 바꿔야함

  4. validation 파이프는 예외를 던지거나 입력값을 그대로 리턴함

자 이제 파이프를 바인드해보자.

파이프는 parameter-scoped, method-scoped, controller-scoped, global-scoped가 가능

이번엔 post body를 검증할 수 있도록, 라우트 핸들러의 @Body() 데코레이터에 파이프를 추가해볼거임

@Post()
async create(
	@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
	this.catsService.create(createCatDto);
}

Global scoped pipes

ValidationPipe는 가능한 generic하게 만들어졌기 때무넹, 글로벌 파이프로 설정할 경우 모든 라우트 핸들러에 적용이 가능하다.

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

글로벌 파이프는 모듈 컨텍스트의 밖에서 바인딩되기 때문에, DI는 불가능하다.

이 문제를 해결하려면 글로벌 파이프를 모듈에서 직접 설정하면 된다.

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

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

Providing defaults

Parse* 파이프는 null이나 undefined를 받으면 예외를 던진다.

디폴트 값을 설정함으로써 예외를 던지는 것 대신 엔드포인트에서 빠진 파라미터에 대해 처리하도록 할 수 있다.

DefaultValuePipe를 사용하면 됨

@Get()
async findAll(
  @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
  @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
  return this.catsService.findAll({ activeOnly, page });
}
profile
nakkim.hashnode.dev로 이사합니다
post-custom-banner

0개의 댓글