본문 바로가기

내일배움캠프 노드 4기/Today I Learned

[Nest.js] DTO

Nest.js에서 클라이언트와 데이터 통신을 하기 위해서는 DTO(Data Transfer Object)를 사용해야 합니다.

DTO는 데이터를 전송하기 위해 작성된 객체로 Nest.js에서는 모든 데이터를 DTO로 주고받습니다.

 

DTO를 작성하기 위한 패키지 설치

$ npm i class-validator class-transformer

 

DTO 클래스 작성

// create-article.dto.ts
import { IsNumber, IsString } from 'class-validator';

export class CreateArticleDto {
  @IsString()
  readonly title: string;

  @IsString()
  readonly content: string;

  @IsNumber()
  readonly password: number;
}

@IsString, @IsNumber  데코레이터는 Nest.js에서 제공하는 데코레이터가 아닌

class-validator에서 제공하는 데코레이터 입니다.

위의 코드에서는 클라이언트로부터 전달받아야 하는 title, content를 DTO 객체의 변수로 선언했고 title, content 둘 중 하나라도 String 값으로 오지 않으면 자동으로 400 에러를 리턴합니다.

 

DTO는 통상적으로 요청(Get, Post, Put, Delete) 1개에 1개를 만듭니다.

하지만 전달받는 데이터가 같은(중복되는) DTO가 생길 수 있는데

그럴 때는 @nestjs/mapped-types의 PartialType을 상속받으면 됩니다.

상속받은 클래스에서 필요한 데이터만 뽑아서 사용하면 됩니다.

 

그중 일부만을 선택해서 사용하고 싶다면

@nestjs/mapped-types의 PickType을 상속받아 나는 이 부모 클래스에서 이 필드만 필요해 라고 선언하면 됩니다.

 

@nestjs/mapped-types 설치 코드

$ npm i @nestjs/mapped-types

update-article.dto.ts 코드 // PartialType 사용

import { PartialType } from '@nestjs/mapped-types';
import { CreateArticleDto } from './create-article.dto';

export class UpdateArticleDto extends PartialType(CreateArticleDto) {}

delete-article.dto.ts 코드 // PickType 사용

import { PickType } from '@nestjs/mapped-types';
import { CreateArticleDto } from './create-article.dto';

export class DeleteArticleDto extends PickType(CreateArticleDto, [
  'password',
] as const) {}

 

DTO의 유효성 검사

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()); // 이 한줄만 넣어주면 됩니다! 잊지마세요!  
  await app.listen(3000);
}
bootstrap();

 

컨트롤러 코드

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { DeleteArticleDto } from './dto/delete-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';

@Controller('board')
export class BoardController {
  constructor(private readonly boardService: BoardService) {}

  @Get('/articles')
  getArticles() {
    return this.boardService.getArticles();
  }

  @Get('/articles/:id')
  getArticleById(@Param('id') articleId: number) {
    return this.boardService.getArticleById(articleId);
  }

  @Post('/articles')
  createArticle(@Body() data: CreateArticleDto) {
    return this.boardService.createArticle(
      data.title,
      data.content,
      data.password,
    );
  }

  @Put('/articles/:id')
  updateArticle(
    @Param('id') articleId: number,
    @Body() data: UpdateArticleDto,
  ) {
    return this.boardService.updateArticle(
      articleId,
      data.title,
      data.content,
      data.password,
    );
  }

  @Delete('/articles/:id')
  deleteArticle(
    @Param('id') articleId: number,
    @Body() data: DeleteArticleDto,
  ) {
    return this.boardService.deleteArticle(articleId, data.password);
  }
}

서비스 코드

import {
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import _ from 'lodash';

@Injectable()
export class BoardService {
  // 데이터베이스를 사용하지 않아 일단은 배열로 구현을 하였으니 오해 말아주세요!
  // 보통은 TypeORM 모듈을 이용하여 리포지토리를 의존합니다. 이건 나중에 배울게요!
  private articles = [];

  // 게시글 비밀번호를 저장하기 위한 Map 객체입니다.
  private articlePasswords = new Map();

  getArticles() {
    return this.articles;
  }

  getArticleById(id: number) {
    return this.articles.find((article) => return article.id === id);
  }

  createArticle(title: string, content: string, password: number) {
    const articleId = this.articles.length + 1;
    this.articles.push({ id: articleId, title, content });
    this.articlePasswords.set(articleId, password);
    return articleId;
  }

  updateArticle(id: number, title: string, content: string, password: number) {
    if (this.articlePasswords.get(id) !== password) {
      throw new UnauthorizedException(
        `Article password is not correct. id: ${id}`,
      );
    }

    const article = this.getArticleById(id);
    if (_.isNil(article)) {
      throw new NotFoundException(`Article not found. id: ${id}`);
    }

    article.title = title;
    article.content = content;
  }

  deleteArticle(id: number, password: number) {
    if (this.articlePasswords.get(id) !== password) {
      throw new UnauthorizedException(
        `Article password is not correct. id: ${id}`,
      );
    }

    this.articles = this.articles.filter((article) => article.id !== id);
  }
}