반응형
7장: 실전 프로젝트 - Todo 리스트 API 서버 만들기
지금까지 배운 NestJS의 핵심 개념(모듈, 컨트롤러, 프로바이더, DI, TypeORM, 파이프 등)을 모두 활용하여 할 일(Todo) 목록을 관리하는 완전한 CRUD API 서버를 구축합니다.
1. 프로젝트 목표 및 구조
기능 요구사항:
- Todo 목록 조회 (완료/미완료 필터링 기능 포함)
- 특정 Todo 조회
- 새 Todo 생성 (내용 유효성 검사 포함)
- Todo 내용 및 완료 상태 수정
- 특정 Todo 삭제
기술 스택:
- 프레임워크: NestJS
- 데이터베이스: TypeORM + SQLite (간단한 파일 기반 DB로 설정이 쉬움)
- 유효성 검사:
class-validator,class-transformer
프로젝트 구조:
todo-api/ ├── src/ │ ├── app.module.ts │ ├── main.ts │ └── todos/ │ ├── entities/todo.entity.ts │ ├── dto/ │ │ ├── create-todo.dto.ts │ │ └── update-todo.dto.ts │ ├── todos.controller.ts │ ├── todos.module.ts │ └── todos.service.ts └── ... (기타 설정 파일)
2. 구현 단계
1단계: 프로젝트 생성 및 기본 설정
# 1. 새 NestJS 프로젝트 생성
nest new todo-api
cd todo-api
# 2. 필요한 라이브러리 설치
npm install @nestjs/typeorm typeorm sqlite3 class-validator class-transformer
2단계: 데이터베이스 연동 및 AppModule 설정
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite', // SQLite 사용
database: 'todo.db', // 프로젝트 루트에 todo.db 파일 생성
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true, // 개발용으로 스키마 자동 동기화
}),
TodosModule, // Todos 기능 모듈 임포트
],
})
export class AppModule {}
3단계: Todos 모듈, 엔티티, DTO 생성
# Todos 모듈, 컨트롤러, 서비스 한번에 생성
nest g resource todos
- 위 명령은
todos폴더와 그 안의 기본 파일들, 그리고Todo엔티티와 DTO 클래스까지 생성해줍니다. 생성된 파일들을 아래와 같이 수정합니다.
// src/todos/entities/todo.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Todo {
@PrimaryGeneratedColumn()
id: number;
@Column()
task: string;
@Column({ default: false })
isDone: boolean;
}
// src/todos/dto/create-todo.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
export class CreateTodoDto {
@IsString()
@IsNotEmpty()
task: string;
}
// src/todos/dto/update-todo.dto.ts
import { IsString, IsBoolean, IsOptional } from 'class-validator';
export class UpdateTodoDto {
@IsString()
@IsOptional() // task는 선택적으로 업데이트 가능
task?: string;
@IsBoolean()
@IsOptional() // isDone도 선택적으로 업데이트 가능
isDone?: boolean;
}
4단계: TodosService에 CRUD 로직 구현
todos.service.ts 파일에 TypeORM 리포지토리를 주입받아 각 비즈니스 로직을 완성합니다.
// src/todos/todos.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Todo } from './entities/todo.entity';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Injectable()
export class TodosService {
constructor(
@InjectRepository(Todo)
private todosRepository: Repository<Todo>,
) {}
create(createTodoDto: CreateTodoDto): Promise<Todo> {
const todo = this.todosRepository.create(createTodoDto);
return this.todosRepository.save(todo);
}
findAll(isDone?: boolean): Promise<Todo[]> {
if (isDone !== undefined) {
return this.todosRepository.find({ where: { isDone } });
}
return this.todosRepository.find();
}
async findOne(id: number): Promise<Todo> {
const todo = await this.todosRepository.findOneBy({ id });
if (!todo) {
throw new NotFoundException(`Todo with ID ${id} not found`);
}
return todo;
}
async update(id: number, updateTodoDto: UpdateTodoDto): Promise<Todo> {
const todo = await this.todosRepository.preload({ id, ...updateTodoDto });
if (!todo) {
throw new NotFoundException(`Todo with ID ${id} not found`);
}
return this.todosRepository.save(todo);
}
async remove(id: number): Promise<void> {
const result = await this.todosRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`Todo with ID ${id} not found`);
}
}
}
5단계: TodosController 구현 및 ValidationPipe 적용
todos.controller.ts에서 서비스의 메소드를 호출하고, 파이프를 적용합니다.
// src/todos/todos.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, ValidationPipe, ParseBoolPipe, DefaultValuePipe } from '@nestjs/common';
import { TodosService } from './todos.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
create(@Body(ValidationPipe) createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}
@Get()
findAll(@Query('isDone', new DefaultValuePipe(undefined), ParseBoolPipe) isDone?: boolean) {
return this.todosService.findAll(isDone);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.todosService.findOne(id);
}
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body(ValidationPipe) updateTodoDto: UpdateTodoDto) {
return this.todosService.update(id, updateTodoDto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.todosService.remove(id);
}
}
- 참고:
@Patch는 리소스의 부분 수정을 의미하므로PUT보다 더 적합합니다. main.ts에app.useGlobalPipes(new ValidationPipe({ whitelist: true }));를 추가하면 컨트롤러에서ValidationPipe를 반복적으로 선언하지 않아도 됩니다.whitelist: true옵션은 DTO에 정의되지 않은 속성이 들어오면 자동으로 제거하는 유용한 기능입니다.
3. 연습 문제 및 개선 과제
문제 1: 검색 기능 추가
- 요구사항:
GET /todos엔드포인트에task쿼리 파라미터를 추가하여, 특정 키워드가 포함된 Todo만 검색하는 기능을 구현하세요. - 세부사항:
TodosService의findAll메소드를 수정하여,task문자열을 인자로 받도록 합니다.- TypeORM의
Like연산자를 사용하여task컬럼에 대한 부분 일치 검색을 구현합니다. (예:this.todosRepository.find({ where: { task: Like(%${task}%) } })) TodosController의findAll메소드에서@Query('task')로 검색어를 받아 서비스에 전달합니다.
문제 1 힌트
// src/todos/todos.service.ts
import { Like } from 'typeorm';
// ...
findAll(isDone?: boolean, task?: string): Promise<Todo[]> {
const where: any = {};
if (isDone !== undefined) where.isDone = isDone;
if (task) where.task = Like(`%${task}%`);
return this.todosRepository.find({ where });
}
// ...
문제 2: 사용자(User)와 Todo 관계 맺기
- 요구사항:
User엔티티를 만들고, 하나의User가 여러 개의Todo를 가질 수 있는 일대다(One-to-Many) 관계를 설정하세요. - 세부사항:
User엔티티를 생성합니다. (id,name등)User엔티티에는@OneToMany(() => Todo, todo => todo.user)데코레이터로todos속성을 추가합니다.Todo엔티티에는@ManyToOne(() => User, user => user.todos)데코레이터로user속성을 추가하여 관계를 맺습니다. 이렇게 하면todo테이블에userId외래 키(Foreign Key)가 생성됩니다.- Todo를 생성할 때 어떤 사용자의 Todo인지
userId를 함께 받도록 로직을 수정합니다. - 특정 사용자의 Todo 목록만 조회하는 API(예:
GET /users/:userId/todos)를 만들어봅니다.
문제 2 힌트
// src/users/entities/user.entity.ts
// ...
@OneToMany(() => Todo, todo => todo.user)
todos: Todo[];
// src/todos/entities/todo.entity.ts
// ...
@ManyToOne(() => User, user => user.todos)
user: User;
반응형
'백엔드 > 네스트' 카테고리의 다른 글
| [NestJS] 10장: 테스팅 - 견고한 애플리케이션 만들기 (0) | 2025.09.19 |
|---|---|
| [NestJS] 9장: 설정 및 로깅 - 애플리케이션 환경 관리하기 (0) | 2025.09.19 |
| [NestJS] 8장: 인증 - 사용자를 확인하고 보호하기 (0) | 2025.09.19 |
| [NestJS] 6장: 데이터베이스 연동 (with TypeORM) (0) | 2025.09.19 |
| [NestJS] 5장: 미들웨어, 파이프, 가드 - 요청 처리의 수문장들 (0) | 2025.09.19 |
| [NestJS] 4장: 모듈 - 코드의 체계적인 정리와 캡슐화 (0) | 2025.09.19 |
| [NestJS] 3장: 프로바이더와 의존성 주입 - 로직의 분리와 재사용 (0) | 2025.09.18 |
| [NestJS] 2장: 컨트롤러와 라우팅 - 요청을 받아들이는 관문 (0) | 2025.09.18 |