[NestJS 공식문서 정독하기] Overview - Pipes
2022. 8. 2. 14:26ㆍWeb/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
- Typescript를 꼭 사용해야 해당 기능을 사용할 수 있음 (Javascript는 안됨)
- class-validator, class-transformer 라이브러리 사용
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에 도달하기 전에 특정 타입으로 변환이 되어야 하거나, 특정 필드가 없을 때 기본값을 추가해서 전달하는 과정이 필요할 때 유용함
반응형