본문 바로가기

NestJS로 구현하는 게시판 API 실전 가이드

민이(MInE) 2024. 11. 11.
반응형

목차

  1. 프로젝트 구조
  2. 도메인 모델 설계
  3. API 엔드포인트 구현
  4. 유효성 검증
  5. 에러 처리
  6. 테스트

프로젝트 구조

src/boards/
├── constants/
│   └── board.constants.ts   # 상수 정의
├── dto/
│   └── create-board.dto.ts  # DTO 클래스
├── pipes/
│   └── board-status-validation.pipe.ts # 커스텀 파이프
├── board.model.ts           # 도메인 모델
├── boards.controller.ts     # 컨트롤러
├── boards.service.ts        # 서비스
└── boards.module.ts         # 모듈

도메인 모델 설계

게시글 모델 정의

// board.model.ts
export interface Board {
  id: string;
  title: string;
  description: string;
  status: BoardStatus;
}

export enum BoardStatus {
  PUBLIC = 'PUBLIC',
  PRIVATE = 'PRIVATE',
}

DTO 설계

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

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

  @IsNotEmpty()
  description: string;
}

API 엔드포인트 구현

컨트롤러 구현

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

  // 게시글 목록 조회
  @Get()
  getAllBoards(): Board[] {
    return this.boardsService.getAllBoards();
  }

  // 게시글 생성
  @Post()
  @HttpCode(HttpStatus.CREATED)
  @UsePipes(ValidationPipe)
  createBoard(@Body() createBoardDto: CreateBoardDto): Board {
    return this.boardsService.createBoard(createBoardDto);
  }

  // 게시글 상세 조회
  @Get(':id')
  getBoardById(
    @Param('id', new ParseUUIDPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) id: string
  ): Board {
    return this.boardsService.getBoardById(id);
  }

  // 게시글 상태 업데이트
  @Patch(':id/status')
  updateBoardStatus(
    @Param('id', ParseUUIDPipe) id: string,
    @Body('status', BoardStatusValidationPipe) status: BoardStatus,
  ): Board {
    return this.boardsService.updateBoardStatus(id, status);
  }

  // 게시글 삭제
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  deleteBoard(@Param('id', ParseUUIDPipe) id: string): void {
    this.boardsService.deleteBoard(id);
  }
}

서비스 로직 구현

@Injectable()
export class BoardsService {
  private boards: Board[] = [];

  // 전체 게시글 조회
  getAllBoards(): Board[] {
    return [...this.boards];
  }

  // 게시글 생성
  createBoard(createboardDto: CreateBoardDto): Board {
    const board: Board = {
      id: randomUUID(),
      ...createboardDto,
      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(BOARD_MESSAGES.NOT_FOUND(id));
    }
    return { ...found };
  }

  // 게시글 상태 업데이트
  updateBoardStatus(id: string, status: BoardStatus): Board {
    const boardIndex = this.boards.findIndex(board => board.id === id);
    if (boardIndex === -1) {
      throw new NotFoundException(BOARD_MESSAGES.NOT_FOUND(id));
    }

    this.boards[boardIndex] = {
      ...this.boards[boardIndex],
      status
    };

    return { ...this.boards[boardIndex] };
  }

  // 게시글 삭제
  deleteBoard(id: string): void {
    const boardIndex = this.findBoardIndex(id);
    this.boards.splice(boardIndex, 1);
  }
}

유효성 검증

커스텀 파이프를 통한 상태값 검증

export class BoardStatusValidationPipe implements PipeTransform<string, BoardStatus> {
  private readonly statusOptions = Object.values(BoardStatus);

  transform(value: string): BoardStatus {
    const upperValue = value?.toUpperCase();

    if (!this.isStatusValid(upperValue)) {
      throw new BadRequestException(BOARD_MESSAGES.INVALID_STATUS(value));
    }

    return upperValue as BoardStatus;
  }

  private isStatusValid(status: string): boolean {
    return this.statusOptions.includes(status as BoardStatus);
  }
}

에러 처리

메시지 상수화

export const BOARD_MESSAGES = {
  NOT_FOUND: (id: string) => `해당 id의 게시글이 없습니다. id: ${id}`,
  INVALID_STATUS: (status: string) => `올바르지 않은 게시글 상태입니다. status=${status}`,
} as const;

구현된 기능들

  1. 게시글 목록 조회 (GET /boards)
    • 전체 게시글 목록 반환
    • 불변성을 위해 복사본 반환
  2. 게시글 생성 (POST /boards)
    • UUID를 통한 고유 ID 생성
    • DTO를 통한 입력값 검증
    • 생성 시 기본 상태는 PUBLIC
  3. 게시글 상세 조회 (GET /boards/:id)
    • UUID 형식 검증
    • 존재하지 않는 게시글 처리
  4. 게시글 상태 변경 (PATCH /boards/:id/status)
    • 커스텀 파이프를 통한 상태값 검증
    • 유효하지 않은 상태값 처리
  5. 게시글 삭제 (DELETE /boards/:id)
    • 204 No Content 응답
    • 존재하지 않는 게시글 처리

핵심 구현 포인트

  1. 불변성 유지
    • 객체의 복사본을 반환하여 의도치 않은 수정 방지
      return [...this.boards];
      return { ...found };
  2. 유효성 검증
    • DTO 데코레이터를 통한 입력값 검증
    • 커스텀 파이프를 통한 상태값 검증
    • UUID 형식 검증
  3. 에러 처리
    • 메시지 상수화로 일관성 있는 에러 메시지 관리
    • 적절한 HTTP 상태 코드 사용
  4. 코드 구조화
    • 책임에 따른 명확한 계층 분리
    • 재사용 가능한 컴포넌트 설계

마치며

이 예제는 NestJS의 핵심 기능들을 활용하여 실제 프로덕션에서 사용할 수 있는 게시판 API를 구현하는 방법을 보여줍니다. 특히 다음과 같은 NestJS의 장점들을 잘 활용했습니다:

  • 데코레이터를 통한 선언적 프로그래밍
  • 파이프를 통한 검증 로직 분리
  • 의존성 주입을 통한 느슨한 결합
  • TypeScript의 타입 시스템 활용

개선 가능한 부분

  1. 데이터베이스 연동 (TypeORM/Prisma)
  2. 인증/인가 구현
  3. 페이지네이션 구현
  4. 검색 기능 추가
  5. 테스트 코드 보강

참고자료

https://kth990303.tistory.com/411

반응형

댓글