Ayden's journal

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

활동하기