파이프는 PipeTransform
인터페이스를 구현, @Injectable()
데코레이터를 사용한다.
일반적인 파이프 이용 사례:
두 경우 모두 (컨트롤러 라우트 핸들러에 의해 처리되는) 인수에 대해 동작
모든 transformation이나 validation은 라우트 핸들러가 호출된 후에 수행된다.
(라우트 핸들러 호출 → 파이프 → 메서드 호출?)
파이프는 빌트인도 많고, 만들어서 쓸 수도 있음
이번 챕터에서는 여러 빌트인 파이프를 소개하고 그들을 라우트 핸들러에 바인드 하는 방법, 그리고 커스텀 파이프를 만들어서 파이프를 구축하는 방법을 알려줌
파이프는 exception zone에서 실행된다. 이것은 파이프가 예외를 던지는 경우 exception layer에서 처리한다는 것을 뜻함. 그래서, 파이프가 예외를 던지고 나면, 컨트롤러 메서드는 실행되지 않음.
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
이 파이프는 transformation의 예시이다.
데이터를 integer로 바꿔줌
파이프를 사용하기 위해서, 파이프 클래스의 인스턴스를 적절한 컨텍스트에 바인드 해야함
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);
}
ParseIntPipe
와 ValidationPipe
를 직접 구현하면서 파이프가 어떻게 생겨먹었는지 알아보자.
일단은 입력값을 받아서 바로 리턴하는 파이프를 만들자.
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)
메서드를 구현해야 함
아래는 ArgumentMetadata 인터페이스를 나타냄
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
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;
}
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를 만드는 건 불가능해.. 왜냐하면 미들웨어가 호출된 핸들러와 해당 매개변수를 포함하여 실행 컨텍스트를 알지 못하기 때문임
그래서.
파이프를 쓴다~
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 pipes에서 transformation 파이프를 바인드하는 방법을 알아봤다.
validation 파이프를 바인딩하는 것도 쉬움
일단 우리는 메서드 콜 레벨에서 파이프를 바인드하고 싶음
JoiValidationPipe
의 인스턴스 생성이것을 @UsePipes()
데코레이터를 이용해서 할 거임
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
(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);
}
}
천천히 위 코드를 살펴보자.
async가 있는 transform()
메서드를 봐라
Nest가 동기/비동기 파이프를 둘 다 지원하기 때문에 가능
class-validator 검증의 일부가 비동기적일 수 있기 때문에 async 사용
toValidate()
현재 인수가 네이티브 자바스크립트 타입이라면(타입스크립트가 아니라면) 검증을 우회시킴 - 검증 데코레이터를 가질 수 없기 때문에
plainToInstance()
검증을 위해, 자바스크립트 객체를 typed 객체로 변환
이걸 하는 이유: post body 객체가 네트워크 요청에서 deserialize 될 때, 타입 정보가 없기 때문
Class-validator는 검증 데코레이터를 사용하기 때문에 들어온 바디를 적절하게 decorate된 객체로 바꿔야함
validation 파이프는 예외를 던지거나 입력값을 그대로 리턴함
자 이제 파이프를 바인드해보자.
파이프는 parameter-scoped, method-scoped, controller-scoped, global-scoped가 가능
이번엔 post body를 검증할 수 있도록, 라우트 핸들러의 @Body() 데코레이터에 파이프를 추가해볼거임
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
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 {}
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 });
}