- 프레임워크: NestJS
- 언어: TypeScript
- 데이터베이스: PostgreSQL
- ORM: TypeORM
- API 문서화: Swagger (
@nestjs/swagger
) - 유효성 검사:
class-validator
,class-transformer
- Node.js >= 18
- npm
- PostgreSQL 데이터베이스
npm install --save @nestjs/config
npm install --save @nestjs/typeorm typeorm pg
npm install --save joi
프로젝트 루트 디렉토리에 .env
파일을 생성하고 다음 내용을 추가합니다:
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=your_password
DATABASE_NAME=your_database
PostgreSQL에 지정한 데이터베이스를 생성합니다.
CREATE DATABASE your_database;
개발 환경에서 애플리케이션을 실행하려면:
npm run start:dev
애플리케이션은 기본적으로 http://localhost:3000
에서 실행됩니다.
애플리케이션이 실행 중일 때, Swagger UI를 통해 API 문서를 확인할 수 있습니다:
http://localhost:3000/api
이 섹션에서는 프로젝트에서 REST API를 어떻게 만들었는지 단계별로 설명합니다.
NestJS CLI를 사용하여 todos
모듈을 생성합니다.
nest generate module todos
todos
모듈 내에 컨트롤러와 서비스를 생성합니다.
nest generate controller todos --no-spec
nest generate service todos --no-spec
Todo
엔티티를 정의하여 데이터베이스 테이블과 매핑합니다.
// src/todos/todo.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
export enum TodoStatus {
IN_PROCESS = 'IN PROCESS',
DONE = 'DONE',
IDLE = 'IDLE',
}
@Entity()
export class Todo {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 36 })
name: string;
@Column('text', { default: '' })
description: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@Column({ nullable: true })
startDateAt: Date | null;
@Column({ nullable: true })
dueDateAt: Date | null;
@Column({
type: 'enum',
enum: TodoStatus,
default: TodoStatus.IDLE,
})
status: TodoStatus;
}
app.module.ts
와 todos.module.ts
에서 TypeORM을 설정하고 Todo
엔티티를 등록합니다.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodosModule } from './todos/todos.module';
import { Todo } from './todos/todo.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
username: process.env.DATABASE_USERNAME || 'postgres',
password: process.env.DATABASE_PASSWORD || 'root',
database: process.env.DATABASE_NAME || 'tcs_project',
entities: [Todo],
synchronize: true,
logging: true,
}),
TodosModule,
],
})
export class AppModule {}
// src/todos/todos.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodosService } from './todos.service';
import { TodosController } from './todos.controller';
import { Todo } from './todo.entity';
@Module({
imports: [TypeOrmModule.forFeature([Todo])],
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
요청 데이터의 유효성 검사를 위해 DTO를 정의하고 class-validator
데코레이터를 사용합니다.
// src/todos/todos.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import {
IsString,
IsOptional,
IsDateString,
Length,
IsEnum,
} from 'class-validator';
import { TodoStatus } from './todo.entity';
export class SwaggerCreateTodosDto {
@ApiProperty({
name: 'name',
required: true,
type: String,
minLength: 4,
maxLength: 36,
})
@IsString()
@Length(4, 36)
name: string;
@ApiProperty({
name: 'description',
required: false,
type: String,
default: '',
})
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ name: 'startDateAt', required: false, type: Date })
@IsOptional()
@IsDateString()
startDateAt?: string | null;
@ApiProperty({ name: 'dueDateAt', required: false, type: Date })
@IsOptional()
@IsDateString()
dueDateAt?: string | null;
@ApiProperty({
name: 'status',
required: false,
type: String,
enum: ['IN PROCESS', 'DONE', 'IDLE'],
})
@IsOptional()
@IsEnum(TodoStatus)
status?: TodoStatus;
}
export class SwaggerPutTodosDto {
@ApiProperty({
name: 'name',
required: false,
type: String,
minLength: 4,
maxLength: 36,
})
@IsOptional()
@IsString()
@Length(4, 36)
name?: string;
@ApiProperty({
name: 'description',
required: false,
type: String,
default: '',
})
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ name: 'startDateAt', required: false, type: Date })
@IsOptional()
@IsDateString()
startDateAt?: string | null;
@ApiProperty({ name: 'dueDateAt', required: false, type: Date })
@IsOptional()
@IsDateString()
dueDateAt?: string | null;
@ApiProperty({
name: 'status',
required: false,
type: String,
enum: ['IN PROCESS', 'DONE', 'IDLE'],
})
@IsOptional()
@IsEnum(TodoStatus)
status?: TodoStatus;
}
TodosService
에서 비즈니스 로직을 처리하고, TypeORM Repository
를 사용하여 데이터베이스와 상호 작용합니다.
// src/todos/todos.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Todo } from './todo.entity';
import { SwaggerCreateTodosDto, SwaggerPutTodosDto } from './todos.dto';
@Injectable()
export class TodosService {
constructor(
@InjectRepository(Todo)
private readonly todosRepository: Repository<Todo>,
) {}
async create(createTodoDto: SwaggerCreateTodosDto): Promise<Todo> {
const todo = this.todosRepository.create(createTodoDto);
return await this.todosRepository.save(todo);
}
async findMany(status?: string): Promise<Todo[]> {
const query = this.todosRepository.createQueryBuilder('todo');
if (status) {
query.where('todo.status = :status', { status });
}
return await query.getMany();
}
async findOne(id: string): Promise<Todo> {
const todo = await this.todosRepository.findOne({ where: { id } });
if (!todo) {
throw new NotFoundException(`Todo with ID ${id} not found`);
}
return todo;
}
async update(id: string, updateTodoDto: SwaggerPutTodosDto): Promise<Todo> {
const todo = await this.findOne(id);
Object.assign(todo, updateTodoDto);
return await this.todosRepository.save(todo);
}
async remove(id: string): Promise<void> {
const result = await this.todosRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`Todo with ID ${id} not found`);
}
}
}
TodosController
에서 HTTP 요청을 처리하고, 서비스 메서드를 호출하여 응답합니다.
// src/todos/todos.controller.ts
import {
Body,
Controller,
Get,
Post,
Put,
Delete,
Param,
Query,
HttpCode,
} from '@nestjs/common';
import { TodosService } from './todos.service';
import { ApiConsumes, ApiQuery } from '@nestjs/swagger';
import { SwaggerCreateTodosDto, SwaggerPutTodosDto } from './todos.dto';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
@HttpCode(201)
@ApiConsumes('application/x-www-form-urlencoded')
async create(@Body() todo: SwaggerCreateTodosDto) {
return await this.todosService.create(todo);
}
@Get()
@ApiQuery({
name: 'status',
required: false,
type: String,
enum: ['IN PROCESS', 'DONE', 'IDLE'],
})
async findMany(@Query('status') status: string) {
return await this.todosService.findMany(status);
}
@Get(':id')
async findOne(@Param('id') id: string) {
return await this.todosService.findOne(id);
}
@Put(':id')
@ApiConsumes('application/x-www-form-urlencoded')
async update(
@Param('id') id: string,
@Body() todo: SwaggerPutTodosDto,
) {
return await this.todosService.update(id, todo);
}
@Delete(':id')
async remove(@Param('id') id: string) {
await this.todosService.remove(id);
return { message: 'Todo deleted successfully' };
}
}
@nestjs/swagger
와 Swagger UI를 사용하여 API 문서를 생성하고 확인할 수 있도록 설정합니다.
- 위의
main.ts
에서 Swagger 설정을 추가했습니다. - 각 컨트롤러와 DTO에
@ApiProperty
,@ApiConsumes
,@ApiQuery
등의 Swagger 데코레이터를 사용하여 API 문서를 풍부하게 만들었습니다.
- 엔티티를 정의하고
TypeOrmModule.forFeature([엔티티])
에 등록하면 TypeORM은 자동으로 데이터베이스에 해당 테이블을 생성합니다. synchronize: true
옵션을 활성화하여 애플리케이션이 시작될 때 엔티티 정의에 따라 데이터베이스 스키마를 동기화합니다.- 주의:
synchronize: true
옵션은 개발 환경에서만 사용하는 것이 좋습니다. 프로덕션 환경에서는 데이터베이스 마이그레이션을 사용하는 것이 안전합니다.
src/
├── app.module.ts
├── main.ts
└── todos/
├── todo.entity.ts
├── todos.controller.ts
├── todos.dto.ts
├── todos.module.ts
└── todos.service.ts
@nestjs/common
: ^7.0.0@nestjs/core
: ^7.0.0@nestjs/typeorm
: ^7.0.0@nestjs/swagger
: ^4.0.0typeorm
: ^0.2.29pg
: ^8.0.0class-validator
: ^0.12.2class-transformer
: ^0.2.3
프로젝트 개발 중 설치한 npm 패키지들은 다음과 같습니다:
-
NestJS 및 핵심 의존성
npm install @nestjs/common @nestjs/core @nestjs/platform-express
-
TypeORM 및 PostgreSQL 드라이버
npm install @nestjs/typeorm typeorm pg
-
유효성 검사 및 변환 패키지
npm install class-validator class-transformer
-
Swagger를 사용한 API 문서화
npm install @nestjs/swagger swagger-ui-express
-
Reflect Metadata
npm install reflect-metadata
-
개발 관련 패키지
npm install --save-dev typescript ts-node
-
Joi
npm install joi
src/
├── app.module.ts
├── main.ts
└── todos/
├── todos.controller.ts
├── todos.service.ts
├── todos.module.ts
├── todo.entity.ts
└── todos.dto.ts
@nestjs/common
: ^7.0.0@nestjs/core
: ^7.0.0@nestjs/typeorm
: ^7.0.0@nestjs/swagger
: ^4.0.0typeorm
: ^0.2.0pg
: ^8.0.0class-transformer
: ^0.2.3
- NestJS 공식 문서: https://docs.nestjs.com/
- TypeORM 공식 문서: https://typeorm.io/
- PostgreSQL 공식 사이트: https://www.postgresql.org/
- Swagger를 사용한 API 문서화: https://docs.nestjs.com/openapi/introduction
- 원인:
TodosModule
에서TypeOrmModule.forFeature([Todo])
를imports
에 추가하지 않아TodoRepository
를 주입받을 수 없었습니다. - 해결 방법:
todos.module.ts
에TypeOrmModule.forFeature([Todo])
를 추가하여 문제를 해결했습니다.
- 원인: 요청 데이터가 유효성 검사를 통과하지 못해 400 에러가 발생했습니다.
- 해결 방법:
- 요청 데이터가 유효성 검사 스키마에 맞는지 확인했습니다.
Joi
스키마를 수정하여 NestJS의 유효성 검사 방식을 따랐습니다.