08. Swagger를 사용한 백엔드 문서화
Express에서 openAPI를 사용해 한땀한땀 swagger를 설정해나가던 것과는 달리, NestJs에서는 @nestjs/swagger 라이브러리의 힘으로 아주 간단하게 swagger ui를 구현할 수 있다.
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module'; import { DocumentBuilder } from '@nestjs/swagger/dist/document-builder'; // main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); // Swagger 설정 const config = new DocumentBuilder() .setTitle('API 문서 제목') .setDescription('API 설명') .setVersion('API 버전') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('swagger 엔드포인트', app, document); await app.listen(3000); } bootstrap();
이렇게만 해도 @nestjs/swagger 라이브러리가 컨트롤러의 엔드포인트를 찾아서 알아서 화면을 띄워준다. 우리가 할 것은 요청 및 응답 설정 뿐인 것이다!
엔드포인트 설정
NestJs의 다른 많은 것들과 같이 @nestjs/swagger 라이브러리도 데코레이터를 사용해 swagger ui 문서를 간단히 설정할 수 있도록 한다. 엔드포인트마다 @ApiOperation 데코레이터를 사용하게 되는데, 아래 예시에서 확인할 수 있는 프로퍼티 외에도 해당 엔드포인트가 더 이상 사용되지 않는다는 걸 알리는 deprecated 프로퍼티를 사용할 수 있다.
@Get() @ApiOperation({ summary: 'Get all users', description: 'This endpoint returns a list of all users in the system', tags: ['users'] }) getAllUsers() {}
하나의 컨트롤러에 있는 모든 엔드포인트를 같은 태그로 묶고 싶다면 @ApiTags를 사용할 수 있다. 만약 @ApiTags와 @ApiOperation({ tags })를 같이 사용할 경우 @ApiTags의 내용이 @ApiOperation의 내용을 덮어쓴다.
@ApiTags('Report') @Controller('report') export class ReportController { constructor( private reportService: ReportService, private reportLikesService: ReportLikesService, ) {} }
인증/인가 설정
@nestjs/swagger 라이브러리는 swagger securitySchemes에 대응하는 다양한 데코레이터를 지원한다. 기본적으로는 @ApiSecurity를 사용하지만, 세부적인 설정이 생략되는 @ApiBearerAuth, @ApiOAuth2, @ApiCookieAuth 역시 지원한다. 한 가지 주의해야할 점은 브라우저 보안 상의 이슈로 인해 swagger ui가 요청 헤더에 쿠키를 포함하지 못할 수도 있다는 것이다. 관련된 내용은 swagger 공식 문서에서 확인해볼 수 있다.
요청 설정
클라이언트로부터의 요청은 특정 경로를 통해 들어오며, 해당 요청을 처리하기 위한 자원은 쿼리스트링과 패스 파라미터, 요청 바디에 실려있다. 이 각각을 처리하기 위해 @ApiQuery와 @ApiParam, 그리고 @ApiBody를 사용한다. 눈치챈 사람도 있겠지만 @nestjs/swagger 라이브러리가 제공하는 모든 데코레이터는 Api로 시작한다 :)
@Get(':id') @ApiParam({ name: 'id', description: 'The ID of the user', type: String }) getUserById(@Param('id') id: string) { return `User with ID ${id}`; }
@Get() @ApiQuery({ name: 'age', required: false, description: 'Filter users by age', type: Number }) @ApiQuery({ name: 'gender', required: false, description: 'Filter users by gender', type: String }) getUsers(@Query('age') age: number, @Query('gender') gender: string) { return `Users with age ${age} and gender ${gender}`; }
@ApiBody({ schema: { type: 'array', items: { type: 'array', items: { type: 'number', }, }, }, }) async create(@Body() coords: number[][]) {}
참고로 dto를 사용하지 않는 경우 @nestjs/swagger 라이브러리는 위의 데코레이터를 사용하지 않더라도 메소드의 @Body, @Param, @Query를 인식하고 자동으로 요청 자원을 처리해준다. 이 경우 세부적인 옵션들은 ─ 당연하게도 ─ 기본값을 가지게 된다. dto를 사용하는 경우라면 @ApiProperty와 @ApiPropertyOptional를 사용하여 요청 자원을 처리할 수 있다.
export class SignUpDto { @IsNotEmpty() @IsEmail() @ApiProperty({ example: 'example@test.com' }) email: string; @IsNotEmpty() @IsString() @ApiProperty({ example: 'password1234!' }) password: string; @IsOptional() @IsUrl({}, { message: '올바른 URL을 입력해주세요.' }) @ApiPropertyOptional({ example: 'https://example.com' }) profileImage?: string; @IsOptional() @IsString() @ApiPropertyOptional({ example: '안녕하세요' }) bio?: string; }
만약 받을 수 있는 값의 종류가 제한되어있다면 enum 필드를 사용할 수 있다.
export class SearchReportDto { // @IsNotEmpty() @IsString() @ApiProperty() keyword: string; @IsNotEmpty() @IsIn(['createdAt', 'userLiked']) @ApiProperty({ enum: ['createdAt', 'userLiked'] }) orderBy: 'createdAt' | 'userLiked'; @IsNotEmpty() @IsIn(['report', 'book', 'tag', 'user', 'userLiked']) @ApiProperty({ enum: ['report', 'book', 'tag', 'user', 'userLiked'] }) searchType: 'report' | 'book' | 'tag' | 'user' | 'userLiked'; @IsNotEmpty() @IsString() @ApiProperty({ example: '12' }) take: string; @IsNotEmpty() @IsString() @ApiProperty({ example: '0' }) skip: string; }
아쉽게도 @nestjs/mapped-types에서 제공하는 클래스를 사용하면 @ApiProperty의 정보가 사라지게 된다. 이를 해결하기 위해 @nestjs/swagger에서는 별도의 mapped-types를 제공하고 있다. 이를 사용해야 파생된 dto에서도 @ApiProperty의 정보가 유지된다.
import { ApiProperty, ApiPropertyOptional, PickType } from '@nestjs/swagger'; export class SignUpDto { ... } export class SignInDto extends PickType(SignUpDto, [ 'email', 'password', ] as const) {}
덕분에 하나의 dto만 @ApiProperty를 잘 작성해두어도 다른 파생 dto에 자동으로 데코레이터가 따라오는 것을 확인할 수 있다.

이미지 업로드
class FileUploadDto { @ApiProperty({ name: 'image', type: 'string', format: 'binary' }) file: Express.Multer.File; } class FilesUploadDto { @ApiProperty({ name: 'images', type: 'array', format: 'binary' }) files: Express.Multer.File[]; }
@Post('/multi-upload') @UseGuards(AuthGuard) @UseInterceptors(FilesInterceptor('images')) @ApiConsumes('multipart/form-data') @ApiBody({ description: 'List of image', type: FilesUploadDto, }) async putImages(@UploadedFiles() files: Express.Multer.File[]) {}
응답 설정
@ApiResponse를 사용해 응답 형태를 설정할 수 있지만, @nestjs/swagger는 조금 더 세부적으로 나누어진 여러 데코레이터를 제공하고 있다. 이 데코레이터들은 nestjs의 errorException처럼 미리 상태 코드가 등록되어있다. 데코레이터의 목록은 공식 문서에서 확인해볼 수 있다.
@ApiOkResponse({ description: '로그인 성공', type: ResponseMessageDto, example: { message: '로그인 성공' }, }) @Post('signin') async signIn(@Body() signInDto: SignInDto, @Res() res: Response) {}
옵션 프로퍼티 중 type은 dto를 받는데, 만약 dto를 생성할 만큼 복잡한 타입이 아니라면 ─ 혹은 일회성이라면 ─ schema를 사용할 수도 있다.
@ApiOkResponse({ description: '로그인 성공', schema: { type: 'object', properties: { message: { type: 'string' }, }, }, example: { message: '로그인 성공' }, }) @Post('signin') async signIn(@Body() signInDto: SignInDto, @Res() res: Response) {}
블로그의 정보
Ayden's journal
Beard Weard Ayden