게시판 예제로 게시글을 생성, 검색, 수정, 삭제 기능을 구현해보자.
// 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
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;
}
}
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라는 파리미터를 받아서 서비스에 넘겨준다.
여기서부터는 메소드만 짧게 예제를 보여주었다.
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;
}
createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
return this.boardRepository.createBoard(createBoardDto);
}
@Post()
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Promise<Board> {
return this.boardsService.createBoard(createBoardDto);
}
Pipe란?
- @Injectable() 데코레이터로 주석이 달린 클래스
- data transformation과 data validation을 위해서 사용된다.
- Data transformation: 입력 데이터를 원하는 형식으로 변환 (예: 문자열에서 정수로)
- Data validation: 입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달하면 되고 올바르지 않을 때는 예외를 발생
- 컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동한다.
- Nest는 메소드가 호출되기 직전에 파이프를 삽입하고 파이프는 메소드로 향하는 인수를 수신하고 이에 대해 작동한다.
Handler-level Pipes
- 핸들러 레벨에서 @UsePipes()를 이용해서 사용하며 모든 파라미터에 적용됨
Parameter-level Pipes
- 파라미터 레벨의 파이프로 특정한 파라미터에만 적용
Global Pipes
- 글로벌 파이프로 애플리케이션 레벨의 파이프이며 클라이언트에서 들어오는 모든 요청에 적용(main.ts에 작성)
npm i class-validator class-transformer
게시글을 생성할때 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)를 통해 핸들러 레벨의 유효성 체크를 해준다.
remove() vs delete
- remove()
무조건 존재하는 아이템을 remove 메소드를 이용해서 지워야하며 그러지 않으면 에러 발생(404 Error)- delete(): 아이템이 있으면 지우고 없어도 영향이 없음
- remove()는 검색을 하기 때문에 검색+지우기 로 두번 접근을 한다. 때문에 없어도 되는 정보일 경우 delete()를 권장
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}`);
}
}
@Delete('/:id')
deleteBoard(@Param('id', ParseIntPipe) id: number): Promise<void> {
return this.boardsService.deleteBoard(id);
}
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;
}
@Patch('/:id/status')
updateBoardStatus(
@Param('id', ParseIntPipe) id: number,
@Body('status', BoardStatusValidationPipe) status: BoardStatus,
): Promise<Board> {
return this.boardsService.updateBoardStatus(id, status);
}
커스텀 파이프 구현 방법
transform() 메소드
// 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;
}
}
async getAllBoard(): Promise<Board[]> {
return this.boardRepository.find(); // 조건을 주지 않으면 모든 아이템 반환
}
@Get()
getAllBoard(): Promise<Board[]> {
return this.boardsService.getAllBoard();
}
여기서 하나를 추가해서 하자면 전에 회사에서는 저렇게 모두 가져올 경우 모든 아이템의 수가 많을 수도 있기 때문에 검색 조건으로 개수 제한을 해주었던 기억이 난다.
나중에 프로젝트에서 쓴다면 개수 제한과 시작점을 주는 것이 좋겠다.
그리고 반환값도 Promise<Board[]>가 아닌 findAndCount() 메소드를 통해서 Promise<[Board[], number]>를 반환받아 프론트에서 아이템의 갯수를 표시할 때 사용하는 등 했다.