3. Validator & Transformer
1. NestJS 파이프
파이프는 데이터 변형과 유효성 검사를 위해 사용되며, 컨트롤러 라우트 핸들러에 의해 처리되는 인수에 대해 작동한다. NestJS는 메서드가 호출되기 직전에 파이프를 삽입하고, 삽입된 파이프는 메서드로 향하는 인수를 수신하여 데이터 변형 및 유효성 검사를 실시한다.
파이프를 사용하기 위해 필요한 모듈을 설치한다.
2. 내장 파이프를 이용하여 유효성 검사하기
현재 게시물을 생성할 때 이름과 설명에 아무런 값을 주지 않아도 문제 없이 생성이 된다. 이 부분을 파이프를 이용해서 유효성 검사를 처리해 보자.
먼저, 파이프를 사용하려면 다음과 같이 class-validator
를 DTO에 적용해야 한다. 들어오는 값은 비어있으면 안되기 때문에 @IsNotEmpty
데코레이터를 달아준다.
import { IsNotEmpty } from "class-validator";
export class CreateBoardDto {
@IsNotEmpty()
title: string;
@IsNotEmpty()
description: string;
}
이제 컨트롤러의 createBoard()
라우트 핸들러에 유효성 검사를 위한 파이프인 ValidationPipe
를 @UsePipes
데코레이터의 인수로 넣어 적용한다.
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
UsePipes,
ValidationPipe,
} from "@nestjs/common";
import { Board, BoardStatus } from "./board.model";
import { BoardsService } from "./boards.service";
import { CreateBoardDto } from "./dto/create-board.dto";
@Controller("boards")
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get("/")
getAllBoards(): Board[] {
return this.boardsService.getAllBoards();
}
@Post("/")
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
return this.boardsService.createBoard(createBoardDto);
}
@Get("/:id")
getBoardById(@Param("id") id: string): Board {
return this.boardsService.getBoardById(id);
}
@Delete("/:id")
deleteBoard(@Param("id") id: string): void {
this.boardsService.deleteBoard(id);
}
@Patch("/:id/status")
updateBoardStatus(
@Param("id") id: string,
@Body("status") status: BoardStatus
): Board {
return this.boardsService.updateBoardStatus(id, status);
}
}
3. 특정 게시물을 찾을 때 없는 경우 처리하기
현재 특정 게시물을 ID로 가져올 때 없는 ID의 게시물을 가져오려고 하더라도 아무런 문제 없이 처리가 된다. 이런 경우 에러 메시지를 돌려줄 수 있도록 처리해 보자.
import { Injectable, NotFoundException } from "@nestjs/common";
import { Board, BoardStatus } from "./board.model";
import { v1 as uuid } from "uuid";
import { CreateBoardDto } from "./dto/create-board.dto";
@Injectable()
export class BoardsService {
private boards: Board[] = [];
getAllBoards(): Board[] {
return this.boards;
}
createBoard(createBoardDto: CreateBoardDto): Board {
const { title, description } = createBoardDto;
const board: Board = {
id: uuid(),
title,
description,
status: BoardStatus.PUBLIC,
};
this.boards.push(board);
return board;
}
getBoardById(id: string): Board {
const found = this.boards.find((board) => board.id === id);
if (!found) {
throw new NotFoundException(`Can't find Board with id ${id}`);
}
return found;
}
deleteBoard(id: string): void {
this.boards = this.boards.filter((board) => board.id !== id);
}
updateBoardStatus(id: string, status: BoardStatus): Board {
const board = this.getBoardById(id);
board.status = status;
return board;
}
}
4. 없는 게시물을 지우려 할 때 처리하기
위와 마찬가지로 없는 게시물을 지우려 하는 경우에도 에러 메시지를 돌려줄 수 있도록 처리해 보자.
import { Injectable, NotFoundException } from "@nestjs/common";
import { Board, BoardStatus } from "./board.model";
import { v1 as uuid } from "uuid";
import { CreateBoardDto } from "./dto/create-board.dto";
@Injectable()
export class BoardsService {
private boards: Board[] = [];
getAllBoards(): Board[] {
return this.boards;
}
createBoard(createBoardDto: CreateBoardDto): Board {
const { title, description } = createBoardDto;
const board: Board = {
id: uuid(),
title,
description,
status: BoardStatus.PUBLIC,
};
this.boards.push(board);
return board;
}
getBoardById(id: string): Board {
const found = this.boards.find((board) => board.id === id);
if (!found) {
throw new NotFoundException(`Can't find Board with id ${id}`);
}
return found;
}
deleteBoard(id: string): void {
const found = this.getBoardById(id);
this.boards = this.boards.filter((board) => board.id !== found.id);
}
updateBoardStatus(id: string, status: BoardStatus): Board {
const board = this.getBoardById(id);
board.status = status;
return board;
}
}
5. 커스텀 파이프를 이용하여 유효성 검사하기
위에서는 NestJS에서 이미 구성해 놓은 내장 파이프를 사용했다. 필요한 경우 다음과 같이 커스텀 파이프를 만들어서 사용해도 된다.
먼저 커스텀 파이프를 구현하려면 PipeTransform
이라는 인터페이스를 구현해야 한다. 이 인터페이스는 모든 파이프에서 구현해 줘야 하는 인터페이스이고, 모든 파이프는 transform()
메서드를 구현해야 한다. transform()
메서드의 첫 번째 파라미터는 처리가 된 인자의 값(value
)이고, 두 번째 파라미터는 인자에 대한 메타데이터(metadata
)를 포함한 객체를 의미한다. metadata
에는 metatype
, type
, data
프로퍼티가 있다. transform()
메서드에서 리턴된 값은 라우트 핸들러로 전해지고, 만약 예외가 발생하면 클라이언트로 전해진다.
먼저, 커스텀 파이프를 구현하기 위해 src/boards
디렉터리에 pipes
디렉터리를 생성한다.
그리고 BoardStatus
의 유효성 검사를 위한 BoardStatusValidationPipe
를 구현한다. BoardStatus
는 PUBLIC
과 PRIVATE
만 올 수 있기 때문에 이외의 값이 오면 에러를 보내줄 수 있도록 구현한다.
import { BadRequestException, PipeTransform } from "@nestjs/common";
import { BoardStatus } from "../board.model";
export class BoardStatusValidationPipe implements PipeTransform {
readonly StatusOptions = [BoardStatus.PUBLIC, BoardStatus.PRIVATE];
private isStatusValid(status: any): boolean {
return this.StatusOptions.includes(status);
}
transform(value: any) {
value = value.toUpperCase();
if (!this.isStatusValid(value)) {
throw new BadRequestException(`${value} isn't in the status`);
}
return value;
}
}
이제 updateBoardStatus()
라우트 핸들러에 BoardStatusValidationPipe
를 적용한다.
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
UsePipes,
ValidationPipe,
} from "@nestjs/common";
import { Board, BoardStatus } from "./board.model";
import { BoardsService } from "./boards.service";
import { CreateBoardDto } from "./dto/create-board.dto";
import { BoardStatusValidationPipe } from "./pipes/board-status-validation.pipe";
@Controller("boards")
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get("/")
getAllBoards(): Board[] {
return this.boardsService.getAllBoards();
}
@Post("/")
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
return this.boardsService.createBoard(createBoardDto);
}
@Get("/:id")
getBoardById(@Param("id") id: string): Board {
return this.boardsService.getBoardById(id);
}
@Delete("/:id")
deleteBoard(@Param("id") id: string): void {
this.boardsService.deleteBoard(id);
}
@Patch("/:id/status")
updateBoardStatus(
@Param("id") id: string,
@Body("status", BoardStatusValidationPipe) status: BoardStatus
): Board {
return this.boardsService.updateBoardStatus(id, status);
}
}