NestJS Swagger 통합이란?
NestJS는 @nestjs/swagger 패키지로 OpenAPI 3.0 문서를 자동 생성합니다. 하지만 기본 설정만으로는 불완전한 문서가 만들어집니다. 데코레이터를 정밀하게 활용해야 프론트엔드 개발자가 바로 사용할 수 있는 API 문서가 됩니다. 이 글에서는 @ApiProperty부터 제네릭 응답 래퍼, 파일 업로드, 인증 헤더, CLI 플러그인까지 심화 패턴을 다룹니다.
기본 설정과 SwaggerModule
먼저 Swagger UI를 부트스트랩에 등록하는 기본 설정입니다.
// main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('My API')
.setDescription('API 문서')
.setVersion('1.0')
.addBearerAuth(
{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
'access-token',
)
.addApiKey(
{ type: 'apiKey', in: 'header', name: 'X-API-KEY' },
'api-key',
)
.addServer('https://api.example.com', 'Production')
.addServer('http://localhost:3000', 'Local')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, {
swaggerOptions: {
persistAuthorization: true, // 새로고침 시 토큰 유지
tagsSorter: 'alpha',
operationsSorter: 'alpha',
},
});
await app.listen(3000);
}
DTO 데코레이터 정밀 설정
DTO의 @ApiProperty는 Swagger 문서의 핵심입니다. 타입, 예시, 제약 조건을 명확히 정의해야 합니다.
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEmail, IsEnum, IsInt, Min, Max, IsOptional } from 'class-validator';
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
export class CreateUserDto {
@ApiProperty({
description: '사용자 이메일',
example: 'user@example.com',
format: 'email',
maxLength: 255,
})
@IsEmail()
email: string;
@ApiProperty({
description: '사용자 이름',
example: '홍길동',
minLength: 2,
maxLength: 50,
})
name: string;
@ApiProperty({
description: '사용자 나이',
example: 25,
minimum: 1,
maximum: 150,
type: Number,
})
@IsInt()
@Min(1)
@Max(150)
age: number;
@ApiProperty({
description: '사용자 역할',
enum: UserRole,
enumName: 'UserRole', // 스키마에 이름 부여
default: UserRole.USER,
})
@IsEnum(UserRole)
role: UserRole;
@ApiPropertyOptional({
description: '프로필 이미지 URL',
example: 'https://cdn.example.com/avatar.jpg',
nullable: true,
})
@IsOptional()
avatarUrl?: string | null;
@ApiProperty({
description: '관심 태그 목록',
example: ['nestjs', 'typescript'],
type: [String],
isArray: true,
})
tags: string[];
}
중첩 객체와 배열 타입
중첩 DTO는 Swagger가 자동 인식하지 못하는 경우가 많습니다. type 속성을 명시해야 합니다. NestJS DI Scope 설계와 마찬가지로, 타입 명확성이 핵심입니다.
// 중첩 DTO
export class AddressDto {
@ApiProperty({ example: '서울시 강남구' })
street: string;
@ApiProperty({ example: '06000' })
zipCode: string;
}
export class CreateCompanyDto {
@ApiProperty({ example: 'ACME Corp' })
name: string;
// 단일 중첩 객체
@ApiProperty({ type: () => AddressDto })
headquarters: AddressDto;
// 중첩 객체 배열
@ApiProperty({ type: () => [AddressDto], isArray: true })
branches: AddressDto[];
// Map/Record 타입
@ApiProperty({
type: 'object',
additionalProperties: { type: 'string' },
example: { ko: '에이씨엠이', en: 'ACME' },
})
localizedNames: Record<string, string>;
}
응답 타입 데코레이터
컨트롤러 메서드에 응답 타입을 명시하면 Swagger가 정확한 응답 스키마를 생성합니다.
import {
ApiTags, ApiOperation, ApiResponse, ApiParam,
ApiQuery, ApiBearerAuth, ApiBody, ApiConsumes,
} from '@nestjs/swagger';
@ApiTags('Users')
@ApiBearerAuth('access-token')
@Controller('users')
export class UserController {
@Get()
@ApiOperation({ summary: '사용자 목록 조회' })
@ApiQuery({ name: 'page', required: false, type: Number, example: 1 })
@ApiQuery({ name: 'limit', required: false, type: Number, example: 20 })
@ApiQuery({
name: 'role',
required: false,
enum: UserRole,
description: '역할로 필터링',
})
@ApiResponse({
status: 200,
description: '사용자 목록',
type: [UserResponseDto],
})
findAll(
@Query('page') page?: number,
@Query('limit') limit?: number,
@Query('role') role?: UserRole,
): Promise<UserResponseDto[]> {
return this.userService.findAll({ page, limit, role });
}
@Get(':id')
@ApiOperation({ summary: '사용자 상세 조회' })
@ApiParam({ name: 'id', type: Number, description: '사용자 ID' })
@ApiResponse({ status: 200, type: UserResponseDto })
@ApiResponse({ status: 404, description: '사용자를 찾을 수 없음' })
findOne(@Param('id', ParseIntPipe) id: number): Promise<UserResponseDto> {
return this.userService.findOne(id);
}
@Post()
@ApiOperation({ summary: '사용자 생성' })
@ApiResponse({ status: 201, type: UserResponseDto })
@ApiResponse({ status: 409, description: '이메일 중복' })
create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
return this.userService.create(dto);
}
}
제네릭 응답 래퍼 패턴
실전 API는 { data, meta, message } 같은 공통 응답 래퍼를 사용합니다. Swagger에서 제네릭 래퍼를 표현하는 패턴입니다.
// 페이지네이션 메타
export class PaginationMeta {
@ApiProperty({ example: 1 })
page: number;
@ApiProperty({ example: 20 })
limit: number;
@ApiProperty({ example: 150 })
total: number;
@ApiProperty({ example: 8 })
totalPages: number;
}
// 제네릭 래퍼를 위한 팩토리 함수
export function ApiPaginatedResponse<T extends Function>(itemType: T) {
return applyDecorators(
ApiExtraModels(PaginationMeta, itemType as any),
ApiResponse({
status: 200,
schema: {
allOf: [
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(itemType as any) },
},
meta: { $ref: getSchemaPath(PaginationMeta) },
message: { type: 'string', example: 'OK' },
},
},
],
},
}),
);
}
// 사용
@Get()
@ApiPaginatedResponse(UserResponseDto)
findAll(@Query() query: PaginationQueryDto) {
return this.userService.findAll(query);
}
파일 업로드 문서화
NestJS 파일 업로드 엔드포인트를 Swagger에 정확히 표현하는 방법입니다.
// 단일 파일 업로드
@Post('avatar')
@ApiOperation({ summary: '프로필 이미지 업로드' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
description: '이미지 파일 (JPG, PNG, max 5MB)',
},
description: {
type: 'string',
example: '프로필 사진',
},
},
required: ['file'],
},
})
@UseInterceptors(FileInterceptor('file'))
uploadAvatar(
@UploadedFile() file: Express.Multer.File,
@Body('description') description?: string,
) {
return this.uploadService.uploadAvatar(file, description);
}
// 다중 파일 업로드
@Post('gallery')
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
files: {
type: 'array',
items: { type: 'string', format: 'binary' },
description: '최대 10개 이미지',
},
},
},
})
@UseInterceptors(FilesInterceptor('files', 10))
uploadGallery(@UploadedFiles() files: Express.Multer.File[]) {
return this.uploadService.uploadGallery(files);
}
커스텀 데코레이터 조합
반복되는 Swagger 데코레이터를 커스텀 데코레이터로 묶으면 코드가 깔끔해집니다.
import { applyDecorators } from '@nestjs/common';
// 공통 에러 응답 데코레이터
export function ApiCommonErrors() {
return applyDecorators(
ApiResponse({ status: 400, description: '잘못된 요청' }),
ApiResponse({ status: 401, description: '인증 필요' }),
ApiResponse({ status: 403, description: '권한 없음' }),
ApiResponse({ status: 500, description: '서버 오류' }),
);
}
// 인증 + 역할 데코레이터
export function ApiAuthEndpoint(summary: string) {
return applyDecorators(
ApiOperation({ summary }),
ApiBearerAuth('access-token'),
ApiCommonErrors(),
);
}
// 사용 — 한 줄로 정리
@Get(':id')
@ApiAuthEndpoint('사용자 상세 조회')
@ApiResponse({ status: 200, type: UserResponseDto })
@ApiResponse({ status: 404, description: '사용자를 찾을 수 없음' })
findOne(@Param('id') id: number) {
return this.userService.findOne(id);
}
DTO 상속과 PartialType/PickType
NestJS Swagger는 DTO 변환 유틸리티를 제공하여 중복 정의를 줄입니다.
import {
PartialType, PickType, OmitType, IntersectionType,
} from '@nestjs/swagger';
// PartialType: 모든 필드를 optional로
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// PickType: 특정 필드만 선택
export class LoginDto extends PickType(CreateUserDto, ['email'] as const) {
@ApiProperty({ example: 'P@ssw0rd!' })
password: string;
}
// OmitType: 특정 필드 제외
export class UserListItemDto extends OmitType(UserResponseDto, ['tags'] as const) {}
// IntersectionType: 두 DTO 합치기
export class CreateUserWithAddressDto extends IntersectionType(
CreateUserDto,
AddressDto,
) {}
CLI 플러그인으로 보일러플레이트 제거
Swagger CLI 플러그인을 활성화하면 @ApiProperty를 대부분 생략할 수 있습니다. TypeScript 타입에서 자동 추론합니다.
// nest-cli.json
{
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": true, // class-validator → Swagger 매핑
"introspectComments": true, // JSDoc 주석 → description
"dtoFileNameSuffix": [".dto.ts", ".entity.ts"]
}
}
]
}
}
// 플러그인 활성화 후 — @ApiProperty 생략 가능
export class CreateUserDto {
/** 사용자 이메일 주소 */
@IsEmail()
email: string; // → 자동으로 type: string, description: '사용자 이메일 주소'
/** 사용자 나이 */
@IsInt()
@Min(1)
age: number; // → 자동으로 type: number, minimum: 1
/** 사용자 역할 */
role: UserRole; // → 자동으로 enum 매핑
}
API 그룹화와 태그 전략
대규모 API에서는 태그와 모듈 단위 문서 분리가 중요합니다.
// main.ts — 태그 설명 추가
const config = new DocumentBuilder()
.setTitle('My API')
.addTag('Users', '사용자 관리 API')
.addTag('Auth', '인증·인가 API')
.addTag('Products', '상품 관리 API')
.addTag('Admin', '관리자 전용 API')
.build();
// 모듈별 별도 문서 생성
const publicDoc = SwaggerModule.createDocument(app, config, {
include: [UserModule, ProductModule, AuthModule],
operationIdFactory: (controllerKey, methodKey) =>
`${controllerKey}_${methodKey}`,
});
SwaggerModule.setup('docs/public', app, publicDoc);
const adminDoc = SwaggerModule.createDocument(app, config, {
include: [AdminModule],
});
SwaggerModule.setup('docs/admin', app, adminDoc);
정리
NestJS Swagger는 단순한 문서 생성기가 아니라 API 계약(Contract)의 코드화입니다. @ApiProperty로 타입을 정밀하게 정의하고, 팩토리 함수로 제네릭 래퍼를 표현하며, CLI 플러그인으로 보일러플레이트를 제거하세요. 커스텀 데코레이터 조합으로 반복 코드를 줄이고, PartialType/PickType으로 DTO 변환을 활용하면 유지보수 가능한 API 문서가 완성됩니다.