본문 바로가기
Back-End/🐱NestJS (TypeScript) log

[NestJS 일기] 영화 api 만들기(2) - DTO를 이용해서 입력값 검증하기

by 코딩하는 동현😎 2022. 6. 3.

DTO (Data Transfer Object) 란?

DTO(Data Transfer Object) 는 계층 간 데이터 교환을 하기 위해 사용하는 객체로 , 상대방이 쓰레기 값이나 입력할수 없는 값을 넣을때 검증하는데 쓰일 수 있습니다.

 

dto 폴더를 새로 만들고 , 새 영화를 post방식으로 등록할때(create) body에 대한 입력값 검증을 위해 CreateMovieDto 객체를 만들었습니다.

기존 영화의 정보를 바꿀때도 입력 값의 유효성을 검증하기 위해 UpdateMovieDto를 만들었습니다.


validator , transformer 모듈 설치

class-validator는 유효값 검증해주는 모델이고, class-transformer는 입력값을 자동으로 형 변환 해주는 모듈입니다.

transformer가 없으면 입력값은 무조건 string이기 때문에 id 같은것을 number로 바꿔주려면 다른 절차가 필요합니다.

우선 터미널에 이렇게 입력해서 두 모듈을 설치해줍니다.

$ npm i class-validator class-transformer


CreateMovieDto

title , year , number를 가져야하고 다른 값/ 자료형을 입력 받으면 안됩니다.

import { IsNumber, isString, IsString } from "class-validator"

// data transform object
// 유효성 검증
export class CreateMovieDto {
    @IsString()
    readonly title : string
    @IsNumber()
    readonly year : number
    @IsString({each : true})
    readonly genres : string[]
}

UpdateMovieDto

기존 영화의 특정 정보를 새로 고침하는 업데이트는 create와 동일한 자료를 갖지만, 변경하려는 자료만 가지고 있으면 됩니다.

그러므로 입력값에서 각 필드를 가질수도 있고 안가질수도 있습니다.

//partialType 이용 안할시 코드
export class UpdateMovieDto {
    //필수로 입력해야하는것이 아니기에 ?를 변수 옆에 붙인다.
    @IsString()
    readonly title? : string
    @IsNumber()
    readonly year? : number
    @IsString({each : true})
    readonly genres? : string[]
}

그러나 CreateMovieDto와 동일하고, 상속을 받으면 좋겠다는 생각이 듭니다.

그래서 mapped-types 모듈을 설치해보겠습니다

$ npm i @nestjs/mapped-types

UpdateMovieDto 수정

(CreateMovieDto 상속)

import { IsNumber, isString, IsString } from "class-validator"
import { PartialType } from "@nestjs/mapped-types"
import { CreateMovieDto } from "./create-movie.dto"

// partial type 이용
// 자동으로 기존 dto클래스에서 부분적으로 입력 받을수 있도록
export class UpdateMovieDto extends PartialType(CreateMovieDto) {}

main (useGlobalPipes)

메인 ts파일에 형식 안맞으면 거절하고 , 자동 자료형 변환하도록 json 형식으로 ValidationPipe 인자로 넣어줍니다.

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 유효성 검사 (미들웨어와 흡사)
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted : true,//형식 안맞으면 거절
    transform : true //자동 자료형 변환
  }))
  await app.listen(3000);
}
bootstrap();

자동 형변환이 되므로 기존 파일들의 인자도 바꿔줍니다.

 

movies.controller

moviedata , updatedata를 dto 클래스로 설정해주고,

dto대로 id 등등을 string -> number로 바꿔줍니다.

import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { rename } from 'fs';
import { CreateMovieDto } from './dto/create-movie.dto';
import { UpdateMovieDto } from './dto/update-movie.dto';
import { Movie } from './entities/movie.entities';
import { MoviesService } from './movies.service';

@Controller('movies')
export class MoviesController {
    constructor(private readonly moviesService:MoviesService ){}

    @Get()
    getAll() : Movie[]{
        return this.moviesService.getAll()
    }
    
    @Get('search')
    search(@Query('year') searchingYear: number){
        return `we are going to search for a movie after ${searchingYear}`
    }

    @Get('/:id')
    getOne(@Param('id') movieId:number) : Movie{
        return this.moviesService.getOne(movieId);
    }
    @Post()
    create(@Body() movieData : CreateMovieDto){
        return this.moviesService.create(movieData);
    }
    @Delete('/:id')
    delete(@Param('id') movieId:number){
        return this.moviesService.deleteOne(movieId);
    }

    @Patch('/:id')
    patch(@Param('id') movieId : number, @Body() updateData:UpdateMovieDto){
        return this.moviesService.update(movieId , updateData);
    }

}

movies.service

import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './entities/movie.entities';

@Injectable()
export class MoviesService {
    private movies : Movie[] = [];

    getAll() : Movie[]{
            return this.movies;
    }

    create(movieData){
        this.movies.push({
            id: this.movies.length + 1,
            ...movieData
        })
    }
    getOne(id:number) :Movie{
        const movie = this.movies.find(movie => movie.id === id);
        if (!movie) {
            throw new NotFoundException(`movie with id ${id} not found`);
        }
        return movie;
    }

    deleteOne(id:number){
        this.movies = this.movies.filter(movie => movie.id !== id)
        return;
    }

    update(id:number, updateData ){
        const movie = this.getOne(id)
        this.deleteOne(id)
        this.movies.push({...movie , ...updateData})

    }
}

추가적인 구조 개편

module은 controller와 provider(service)를 가지는데, app.module

은 movie에 대한건 분리시키고,  app.controller와 app.service만 가지는게 좋습니다.

그래서 movie.module을 새로 만들어주고, app 모듈에는 app 관련 컨트롤러와 프로바이더(service)를 넣어주고 , movie 모듈을 import해줍시다.

app 모듈은 기본 페이지고, 한 앱에서 여러개의 모듈로 관리합니다.

(이번 예시는 무비 모듈 하나밖에 없지만)

movies 모듈 생성
$ nest g mo
$ movies

app controller 생성(프로바이더는 생성안할게요)
$ nest g co
$ app

 

app.module

import { Get, Module } from '@nestjs/common';
import { MoviesModule } from './movies/movies.module';
import { AppController } from './app.controller';

@Module({
  // movies 모듈이 추가됨
  imports: [MoviesModule],
  controllers: [AppController],
  providers: [],
})
export class AppModule {

}

movies.module

movies에 대한 컨트롤러와 프로바이더는 이제 무비 모듈에서만 설정해줍니다.

import { Module } from '@nestjs/common';
import { MoviesController } from './movies.controller';
import { MoviesService } from './movies.service';

@Module({
    controllers : [MoviesController],
    providers : [MoviesService]
})
export class MoviesModule {}

app.controller

import { Controller, Get } from '@nestjs/common';

@Controller()
export class AppController {
    
    @Get()
    home(){
        return 'welcome to my app';
    }

    @Get('/name')
    name(){
        return 'creator is donghyun';
    }

}
반응형

댓글