[NestJS] CRUD 예제

soyeon·2023년 4월 11일

Nest

목록 보기
5/10
post-thumbnail

게시판 예제로 게시글을 생성, 검색, 수정, 삭제 기능을 구현해보자.

[READ] ID를 이용해서 특정 게시글 가져오기

Repository

// board.repository.ts
import { Repository } from 'typeorm';
import { Board } from './board.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { CreateBoardDto } from './dto/create-board.dto';
import { BoardStatus } from './board-status.enum';

export class BoardRepository extends Repository<Board> {
  constructor(
    @InjectRepository(Board)
    private boardRepository: Repository<Board>,
  ) {
    super(
      boardRepository.target,
      boardRepository.manager,
      boardRepository.queryRunner,
    );
  }

  async getBoardById(id: number): Promise<Board> {
    return await this.boardRepository.findOneBy({ id });
  }
}

TypeORM 공식문서를 참고하여 필요한 메소드를 찾아서 레포지토리를 작성한다.
getBoardById

  • async/await을 이용하여 데이터베이스 작업이 끝난 후 결과값을 받는다.
  • id 값을 받고 id값이 일치하는 Board를 반환한다.

Service

import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateBoardDto } from './dto/create-board.dto';
import { BoardRepository } from './board.repository';
import { Board } from './board.entity';

@Injectable()
export class BoardsService {
  constructor(private boardRepository: BoardRepository) {}

  async getBoardById(id: number): Promise<Board> {
    const found = await this.boardRepository.getBoardById(id); 
    // found: boardRepository.getBoardById에서 반환 받은 Board

    if (!found) { // 반환된 Board가 없으면 에러 처리
      throw new NotFoundException(`Can't find Board with id ${id}`);
    }
    return found;
  }
}

Controller

import {
  Controller,
  Get,
  Param,
} from '@nestjs/common';
import { BoardsService } from './boards.service';
import { Board } from './board.entity';

@Controller('boards')
export class BoardsController {
  constructor(private boardsService: BoardsService) {}

  @Get('/:id')
  getBoardById(@Param('id') id: number): Promise<Board> {
    return this.boardsService.getBoardById(id);
  }
}

@Param('id') 을 통해서 id라는 파리미터를 받아서 서비스에 넘겨준다.

[CREATE] 게시글 생성하기

여기서부터는 메소드만 짧게 예제를 보여주었다.

Repository

async createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
    const { title, description } = createBoardDto;

    const board = this.create({
      title,
      description,
      status: BoardStatus.PUBLIC,
    });

    await this.save(board);

    return board;
  }

CreateBoardDto

Service

createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
    return this.boardRepository.createBoard(createBoardDto);
  }

Controller

@Post()
  @UsePipes(ValidationPipe)
  createBoard(@Body() createBoardDto: CreateBoardDto): Promise<Board> {
    return this.boardsService.createBoard(createBoardDto);
  }

NestJS Pipe

Pipe란?

  • @Injectable() 데코레이터로 주석이 달린 클래스
  • data transformation과 data validation을 위해서 사용된다.
    - Data transformation: 입력 데이터를 원하는 형식으로 변환 (예: 문자열에서 정수로)
    • Data validation: 입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달하면 되고 올바르지 않을 때는 예외를 발생
  • 컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동한다.
  • Nest는 메소드가 호출되기 직전에 파이프를 삽입하고 파이프는 메소드로 향하는 인수를 수신하고 이에 대해 작동한다.

Pipe 사용하는 법

  • Handler-level Pipes
    - 핸들러 레벨에서 @UsePipes()를 이용해서 사용하며 모든 파라미터에 적용됨

  • Parameter-level Pipes
    - 파라미터 레벨의 파이프로 특정한 파라미터에만 적용

  • Global Pipes
    - 글로벌 파이프로 애플리케이션 레벨의 파이프이며 클라이언트에서 들어오는 모든 요청에 적용(main.ts에 작성)

필요한 모듈

npm i class-validator class-transformer

NestJS에서 구성한 Built-in 파이프를 통해 유효성을 체크해보자.

게시글을 생성할때 title, description에 유효성을 체크하자.
파이프 생성하기

// boards/dtos/create-board.dto.ts
import { IsNotEmpty } from 'class-validator';

export class CreateBoardDto {
  @IsNotEmpty()
  title: string;

  @IsNotEmpty()
  description: title;
}

위와 같이 title과 description은 string 타입이며 null이면 안된다.
만약 string이 아니거나 null이면 에러 발생.
위의 Controller의 코드와 같이 @UsePipes(ValidationPipe)를 통해 핸들러 레벨의 유효성 체크를 해준다.

[DELETE] 게시글 삭제하기

remove() vs delete

  • remove()
    무조건 존재하는 아이템을 remove 메소드를 이용해서 지워야하며 그러지 않으면 에러 발생(404 Error)
  • delete(): 아이템이 있으면 지우고 없어도 영향이 없음
  • remove()는 검색을 하기 때문에 검색+지우기 로 두번 접근을 한다. 때문에 없어도 되는 정보일 경우 delete()를 권장

Service

async deleteBoard(id: number): Promise<void> {
    const result = await this.boardRepository.delete(id);

    if (result.affected === 0) { // DeleteResult { raw: [], affected: 0 } 이면 해당 id값의 Board가 없다.
      throw new NotFoundException(`Can't find Board with id ${id}`);
    }
  }

Controller

@Delete('/:id')
  deleteBoard(@Param('id', ParseIntPipe) id: number): Promise<void> {
    return this.boardsService.deleteBoard(id);
  }

[UPDATE] 게시글 상태 업데이트하기

Service

async updateBoardStatus(id: number, status: BoardStatus): Promise<Board> {
    const board = await this.boardRepository.getBoardById(id);

    board.status = status;
    await this.boardRepository.save(board);
    return board;
  }

Controller

@Patch('/:id/status')
  updateBoardStatus(
    @Param('id', ParseIntPipe) id: number,
    @Body('status', BoardStatusValidationPipe) status: BoardStatus,
  ): Promise<Board> {
    return this.boardsService.updateBoardStatus(id, status);
  }

Custom 파이프를 통해 유효성을 체크해보자.

커스텀 파이프 구현 방법

    1. Pipe Transform이란 인터페이스를 새롭게 만들 커스텀 파이프에 구현한다.
    1. transform() 메소드로 NestJS가 인자를 처리할 수 있도록 한다.

transform() 메소드

  • 두 개의 인자를 가짐
  • 첫번째 파라미터는 처리가 된 인자의 값(value)
  • 두번째 파라미터는 인자에 대한 메타 데이터를 포함한 객체
  • transform()에서 return 된 값은 Route 핸들러로 전해진다. => 예외 발생 시 클라이언트에 바로 전달
  • 사용하기
// boards/pipes/board-status-validation.pipe.ts
import { BadRequestException, PipeTransform } from '@nestjs/common';
import { BoardStatus } from '../board-status.enum';

export class BoardStatusValidationPipe implements PipeTransform {
  readonly StatusOptions = [BoardStatus.PUBLIC, BoardStatus.PRIVATE];

  transform(value: any) {
    value = value.toUpperCase();

    if (!this.isStatusValid(value)) { // StatusOptions 안에 없는 value일 경우 에러 처리
      throw new BadRequestException(`${value} isn't in the status options`);
    }
    return value;
  }

  private isStatusValid(status: any) { // StatusOptions 안에 있는 값인치 판별하는 함수
    const index = this.StatusOptions.indexOf(status);
    return index !== -1;
  }
}

[READ] 게시글 상태 업데이트하기

Service

async getAllBoard(): Promise<Board[]> {
    return this.boardRepository.find(); // 조건을 주지 않으면 모든 아이템 반환
  }

Controller

@Get()
  getAllBoard(): Promise<Board[]> {
    return this.boardsService.getAllBoard();
  }

여기서 하나를 추가해서 하자면 전에 회사에서는 저렇게 모두 가져올 경우 모든 아이템의 수가 많을 수도 있기 때문에 검색 조건으로 개수 제한을 해주었던 기억이 난다.
나중에 프로젝트에서 쓴다면 개수 제한과 시작점을 주는 것이 좋겠다.
그리고 반환값도 Promise<Board[]>가 아닌 findAndCount() 메소드를 통해서 Promise<[Board[], number]>를 반환받아 프론트에서 아이템의 갯수를 표시할 때 사용하는 등 했다.

profile
사부작 사부작

0개의 댓글