프로젝트 폴더 구조를 기능 단위로 나누기
예시 구조
bash
src/
main.ts
app.module.ts
modules/
app/
app.module.ts
app.controller.ts
app.service.ts
health/
health.module.ts
health.controller.ts
todo/
todo.module.ts
todo.controller.ts
todo.service.ts
dto/
create-todo.dto.ts
update-todo.dto.ts
entities/
todo.entity.ts
체크 포인트
modules/<기능명>: 기능별로 묶는 영역controller: HTTP 요청 라우팅/입력 추출 담당service(provider): 비즈니스 로직 담당module: controller/provider를 Nest 컨테이너에 등록하는 조립 단위dto: 요청/응답 데이터 형태 정의entities: 도메인 모델 정의
2) Controller는 무엇을 하는가
공식 문서 핵심
- Controller는 들어오는 요청을 처리하고 응답을 반환하는 계층이다.
@Controller('prefix')로 라우트 프리픽스를 정의한다.@Get,@Post,@Patch,@Delete같은 메서드 데코레이터로 HTTP 메서드를 매핑한다.@Param(),@Body(),@Query()로 요청 데이터를 꺼낸다.
예시 코드
tsx
@ApiTags('todos')
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Get()
findAll(): TodoEntity[] {
return this.todoService.findAll();
}
@Get(':id')
findOne(@Param('id', new ParseUUIDPipe()) id: string): TodoEntity {
return this.todoService.findOne(id);
}
}
확인해야 할 포인트
@Get(':id')를 쓰면@Param('id')로 값을 받는지 확인- 경로 충돌 방지: 정적 라우트와 파라미터 라우트 선언 순서 확인
- 기본 상태코드:
POST기본 201, 그 외 일반적으로 200 - 필요 시
@HttpCode(...)로 상태코드 명시
3) Service(@Injectable)는 무엇을 하는가
공식 문서 핵심
- Provider는 Nest DI 컨테이너가 관리하는 객체다.
@Injectable()은 해당 클래스를 provider로 사용 가능하게 표시한다.- 생성자 주입(constructor injection)으로 다른 provider를 주입받는다.
예시 코드
tsx
@Injectable()
export class TodoService {
private readonly todos: TodoEntity[] = [];
create(title: string, description?: string): TodoEntity {
const todo: TodoEntity = {
id: randomUUID(),
title,
description,
status: TodoStatus.TODO,
createdAt: new Date(),
updatedAt: new Date(),
};
this.todos.push(todo);
return todo;
}
}
Provider 스코프(공식 문서 기준)
- 기본(
DEFAULT)은 싱글톤: 앱 생명주기 동안 인스턴스 1개 공유 REQUEST: 요청마다 새 인스턴스TRANSIENT: 주입받는 곳마다 새 인스턴스
확인해야 할 포인트
- service는 라우팅 데코레이터(
@Get등)를 쓰지 않는다. - service는 데이터 변경 책임을 가진다(
push,update,splice등). - 인메모리 배열은 프로세스 재시작 시 초기화된다.
4) Module은 무엇을 하는가
공식 문서 핵심
@Module()메타데이터 주요 키:controllers: 이 모듈의 컨트롤러providers: 이 모듈의 프로바이더imports: 다른 모듈에서 export한 provider를 가져옴exports: 현재 모듈 provider를 외부 모듈에 공개
- 모듈은 provider를 기본적으로 캡슐화한다.
예시 코드
tsx
// todo.module.ts
@Module({
controllers: [TodoController],
providers: [TodoService],
})
export class TodoFeatureModule {}
tsx
// app.module.ts
@Module({
imports: [AppFeatureModule, HealthModule, TodoFeatureModule],
})
export class AppModule {}
확인해야 할 포인트
- 기능 모듈을 만들었으면
AppModule.imports에 연결했는지 확인 - 연결 누락 시 라우트가 앱에 노출되지 않음
5) API를 만들고 앱에 연결하는 순서 (Todo 기준)
Step 1) Entity 정의
TodoEntity에 필드 정의 (id,title,status,createdAt등)
Step 2) DTO 정의
CreateTodoDto: 생성 입력UpdateTodoDto: 수정 입력(부분 갱신)
Step 3) Service 구현
create,findAll,findOne,update,remove구현
Step 4) Controller 라우트 구현
GET /todosGET /todos/:idPOST /todosPATCH /todos/:idDELETE /todos/:id
Step 5) Module 등록 + AppModule imports 연결
TodoFeatureModule등록 후AppModule.imports에 추가
6) Swagger 문서와 UI 붙이기
공식 문서 핵심
main.ts에서DocumentBuilder로 문서 메타정보 생성SwaggerModule.createDocument(...)로 OpenAPI 문서 생성SwaggerModule.setup(path, app, document)으로 Swagger UI 경로 마운트
예시 코드
tsx
const swaggerConfig = new DocumentBuilder()
.setTitle('Todo API')
.setDescription('Todo API 문서')
.setVersion('1.0.0')
.addTag('health')
.addTag('todos')
.build();
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('docs', app, swaggerDocument, {
customSiteTitle: 'Todo API Swagger',
});
7) 어노테이션(데코레이터) 해석
예시 코드
tsx
@ApiOperation({ summary: '투두 수정' })
@ApiParam({ name: 'id', description: 'Todo UUID' })
@ApiOkResponse({ description: '투두 수정 완료', type: TodoEntity })
@Patch(':id')
update(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() dto: UpdateTodoDto,
): TodoEntity {
return this.todoService.update(id, dto);
}
각 항목 의미
@Patch(':id'): HTTP PATCH를/todos/:id에 매핑@Param('id', new ParseUUIDPipe()): path paramid추출 + UUID 형식 검증@Body(): 요청 본문(JSON)을 DTO로 바인딩@ApiOperation({ summary: ... }): Swagger 문서 operation summary 설정@ApiParam(...): Swagger 문서 path parameter 메타데이터 추가@ApiOkResponse(...): 200 응답 스키마/설명 문서화
같이 자주 쓰는 응답 데코레이터
@ApiCreatedResponse(...): 201 생성 응답 문서화@ApiNoContentResponse(...): 204 무본문 응답 문서화
검증 결과
- 실습 앱
/docs-json에서PATCH /todos/{id}의 summary/parameters/200 응답 반영 확인 DELETE /todos/{id}의 204 응답 반영 확인
8) DTO와 Entity를 왜 분리하는가
DTO가 의미하는 것
- DTO(Data Transfer Object)는 "요청/응답으로 주고받는 데이터 형식"을 명시하는 클래스다.
- 실습 코드 기준 DTO 파일:
src/modules/todo/dto/create-todo.dto.tssrc/modules/todo/dto/update-todo.dto.ts
- Controller에서
@Body()로 DTO를 받을 때, "이 API가 어떤 입력을 받는지"가 코드로 고정된다.
DTO를 어디에 쓰는가
POST /todos의 본문 형식 정의:CreateTodoDtoPATCH /todos/:id의 본문 형식 정의:UpdateTodoDto- Swagger 문서에서 요청 스키마를 생성할 때 사용됨 (
requestBody스키마로 노출)
Entity가 의미하는 것
- Entity는 "서비스 내부에서 다루는 도메인 데이터 구조"다.
- 실습 코드 기준 Entity 파일:
src/modules/todo/entities/todo.entity.ts
- Todo의 실제 상태를 담는 필드(
id,title,status,createdAt,updatedAt)를 가진다.
Entity를 어디에 쓰는가
- Service 내부 저장소(
todos배열)의 타입 - Service 메서드 반환 타입 (
findAll,findOne,create,update) - Swagger 응답 스키마 타입 (
@ApiOkResponse({ type: TodoEntity }))
DTO와 Entity를 분리하는 이유
- 입력 계약과 내부 모델의 책임을 분리할 수 있다.
- 예를 들어
UpdateTodoDto는 모든 필드를 optional로 둘 수 있지만,TodoEntity는 서비스 내부에서 항상 완성된 상태를 유지할 수 있다. - 요청 스펙 변경(예: 입력 필드 추가)과 내부 데이터 구조 변경(예: 상태 필드 확장)을 독립적으로 관리하기 쉽다.
DTO와 Entity에 넣을 것/넣지 말 것
- DTO에 넣을 것:
- 클라이언트가 보낼 수 있는 입력 필드
- 요청 스키마 설명(
@ApiProperty,@ApiPropertyOptional)
- DTO에 넣지 말 것:
- DB 내부 메타데이터(
createdAt자동 생성 로직 등) - 비즈니스 로직
- DB 내부 메타데이터(
- Entity에 넣을 것:
- 서비스/도메인에서 유지해야 하는 실제 데이터 구조
- 내부 상태 필드와 타입(예:
TodoStatus)
- Entity에 넣지 말 것:
- HTTP 라우팅 정보
- 요청 추출 로직(
@Body,@Param)
Todo API에서의 데이터 흐름
- 클라이언트가 JSON 본문을 보낸다.
- Controller가
@Body()로 DTO를 받는다. - Service가 DTO 데이터를 사용해 Entity를 생성/수정한다.
- Service가 Entity를 반환한다.
- Controller는 반환된 Entity를 응답으로 전달한다.
참고 링크
- Controllers: https://docs.nestjs.com/controllers
- Modules: https://docs.nestjs.com/modules
- Providers: https://docs.nestjs.com/providers
- Injection scopes: https://docs.nestjs.com/fundamentals/injection-scopes
- OpenAPI introduction: https://docs.nestjs.com/openapi/introduction
- OpenAPI operations: https://docs.nestjs.com/openapi/operations
- OpenAPI types and parameters: https://docs.nestjs.com/openapi/types-and-parameters