티스토리 뷰
Nest에서 스웨거를 사용하는 방법을 설명합니다.
API 문서화
API 문서화란 API 사용 방법을 문서로 정리하는 것을 말합니다.
보통 엔드포인트와 HTTP 메서드, 요청 및 응답 형식, 인증 방법, 예시 등이 내용으로 들어갑니다.
API 문서는 워드나 노션 이용해 개발자가 직접 수기로 작성할 수도 있고, 스웨거와 같은 도구를 이용할 수도 있습니다.
쉽게 말해 `'API 요청 주소는 여기고요. 요청 파라미터는 이런 것들이 있습니다. 이렇게 호출하면 이런 결과가 나와요~! ✋'`
와 같이 API 사용 설명서를 만드는 일이라고 할 수 있습니다.
공공데이터포털에서 예시를 쉽게 찾아볼 수 있습니다.
왜 필요한가요?
API 사용자의 편의를 위해서 입니다!
사용자는 보통 프론트엔드 개발자나, 해당 API를 이용해 서비스를 구현하려는 다른 개발자가 되겠죠.
API 문서는 API가 제공하는 기능과 기능의 동작 방식에 대해 더 명확하게 설명하고, 불필요한 소통을 줄여줍니다.
또한 올바른 사용법을 안내하여 예상치 못한 오류를 방지하도록 합니다.
즉, API 문서화는 개발자들이 API를 쉽게 이해하고 효율적으로 활용할 수 있도록 도와줍니다. 💡
Swagger
스웨거(Swagger)는 널리 사용되는 API 문서 자동화 도구입니다.
스웨거를 사용하면 별도의 문서를 작성하지 않아도 코드 내에서 API 문서를 만들고 수정할 수 있습니다.
코드와 문서를 따로 관리하게 되면, 둘 중 하나만 수정된 경우 실제 API와 문서상 API의 동작이 달라지는 문제가 발생하겠죠?
스웨거는 여기서 발생하는 불편함을 줄여줍니다.
다른 장점으로는 Swagger UI를 통해 작성된 API를 시각함과 동시에, 테스트할 수 있는 화면을 제공한다는 것입니다.
NestJS에서 Swagger 사용하기
설치
간단히 사용할 수 있는 NestJS 용 패키지가 존재합니다. 아래 명령어로 패키지를 설치합니다. 🎉
npm install --save @nestjs/swagger
swagger.config.ts 작성
이어서 config 파일을 작성합니다. main.ts에 바로 작성해도 되지만, 분리해 두는 것이 가독성에 좋으니까요.
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export function setupSwagger(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle('Test API Docs') // API 명
.setDescription('NestJS Swagger Test') // API 설명
.setVersion('1.0.0') // 버전
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document); // API 문서 주소
}
그리고 main.ts에 setupSwagger()를 추가합니다.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = 3000;
setupSwagger(app);
await app.listen(port);
}
bootstrap();
서버를 작동시키고, http://localhost:3000/api로 접속하면 아래와 같은 화면을 볼 수 있습니다.
swagger를 사용할 준비는 다 되었습니다. 이제 실제로 문서화를 진행해 볼까요?
컨트롤러에 API 명세 추가하기
회원가입을 진행하는 auth모듈의 컨트롤러 코드 일부를 가져왔습니다.
@nestjs/swagger에서 제공하는 데코레이터들을 이용해 API 정보를 추가해 보겠습니다.
(이 글에서는 간단한 몇 개의 데코레이터들만 소개합니다!)
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@HttpCode(201)
@Post('/signup')
async buyerSignUp(@Body() createUserDto: CreateBuyerDto): Promise<void> {
await this.authService.buyerSignUp(createUserDto);
}
...
}
@ApiTags
컨트롤러 클래스에 태그를 지정하는 데 사용됩니다. 각 태그는 스웨거 UI에서 API 문서를 구성할 때 사용됩니다.
@ApiOperation()
컨트롤러의 메서드에 대한 작업(operation)에 대한 설명을 추가하는 데 사용됩니다.
summary 속성에는 간결한 제목을 작성하고, description에 상세 설명을 작성합니다.
@Controller('auth')
@ApiTags('Auth API')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@HttpCode(201)
@Post('/signup')
@ApiOperation({ summary: 'buyer 생성 API', description: 'buyer를 생성한다.' })
async buyerSignUp(@Body() createUserDto: CreateBuyerDto): Promise<void> {
await this.authService.buyerSignUp(createUserDto);
}
...
}
이제 Swagger UI에서 태그와 설명을 확인할 수 있습니다.
DTO에 API 명세 추가하기
지금은 Request body가 비어있는데요. 이곳에 요청 DTO에 대한 정보를 추가해 보겠습니다.
제 프로젝트에서 CreateBuyerDto는 AuthCredentialsDto를 상속받고 있습니다.
따라서 두 클래스에 각각 추가해보겠습니다.
- AuthCredentialsDto
import { IsEmail, Matches } from 'class-validator';
import { IsNotEmptyString } from '../../decorators/is-not-empty-string.decorator';
export class AuthCredentialsDto {
@IsEmail()
email!: string;
@IsNotEmptyString(6, 20)
@Matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&.])[A-Za-z\d@$!%*?&.]{6,}$/, {
message: '비밀번호는 문자, 숫자, 특수문자(@, $, !, %, *, ?, &, .)의 조합으로 6자 이상 20자 이하로 입력해주세요 ',
})
password!: string;
}
- CreateBuyerDto
import { Transform } from 'class-transformer';
import { IsNotEmptyNumber } from '../../decorators/is-not-empty-number.decorator';
import { IsNotEmptyString } from '../../decorators/is-not-empty-string.decorator';
import { BuyerEntity } from '../buyer.entity';
import { AuthCredentialsDto } from './auth-credentials.dto';
export class CreateBuyerDto
extends AuthCredentialsDto
implements Pick<BuyerEntity, 'name' | 'gender' | 'age' | 'phone'>
{
@IsNotEmptyString(1, 128)
name!: string;
@IsNotEmptyNumber()
gender!: number;
@IsNotEmptyNumber()
age!: number;
@Transform(({ value }) => value.replace(/-/g, ''))
@IsNotEmptyString(11, 11)
phone!: string;
}
@ApiProperty()
클래스의 속성(property)에 대한 설명을 추가하는 데 사용됩니다. 주요 속성은 다음과 같습니다.
- type: 속성의 데이터 유형
- description: 속성의 설명
- required: 속성의 필수 여부
- example: 속성의 예시 값
- minLength, maxLength: 문자열 속성의 최소 및 최대 길이
- minimum, maximum: 숫자 속성의 최소 및 최댓값
- enum: 이넘 타입의 경우, 지정가능한 속성 목록
저는 이 중에 type, description, required, example를 추가해 보았습니다.
- AuthCredentialsDto
export class AuthCredentialsDto {
@ApiProperty({ type: String, description: '이메일', required: true, example: 'myemail@gmail.com' })
@IsEmail()
email!: string;
@ApiProperty({
type: String,
description: '비밀번호 (문자, 숫자, 특수문자(@, $, !, %, *, ?, &, .)의 조합으로 6자 이상 20자 이하로 입력해주세요)',
required: true,
example: 'mypassword1!',
})
@IsNotEmptyString(6, 20)
@Matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&.])[A-Za-z\d@$!%*?&.]{6,}$/, {
message: '비밀번호는 문자, 숫자, 특수문자(@, $, !, %, *, ?, &, .)의 조합으로 6자 이상 20자 이하로 입력해주세요 ',
})
password!: string;
}
- CreateBuyerDto
export class CreateBuyerDto
extends AuthCredentialsDto
implements Pick<BuyerEntity, 'name' | 'gender' | 'age' | 'phone'>
{
@ApiProperty({ type: String, description: '이름', required: true, example: 'myname' })
@IsNotEmptyString(1, 128)
name!: string;
@ApiProperty({ type: Number, description: '성별(남자 0, 여자 1)', required: true, example: 1 })
@IsNotEmptyNumber()
gender!: number;
@ApiProperty({ type: Number, description: '나이', required: true, example: 20 })
@IsNotEmptyNumber()
age!: number;
@ApiProperty({
type: String,
description: '휴대전화번호 ( - 는 포함하지 않습니다 )',
required: true,
example: '01012341234',
})
@Transform(({ value }) => value.replace(/-/g, ''))
@IsNotEmptyString(11, 11)
phone!: string;
}
이제 Request body에서 DTO에 대한 예시 및 정보를 확인할 수 있습니다.
하단의 스키마 영역에서는 DTO에 대한 자세한 정보를 확인할 수 있습니다.
테스트해보기
Try it out 버튼을 사용해 스웨거 상에서 API 요청을 날려볼 수 있습니다.
제 서버에는 이미 `myemail@gmail.com` 계정이 존재하기 때문에 에러가 응답으로 날아와야 합니다.
예상대로 에러가 응답값으로 반환되었습니다.
JWT 테스트하기
guard를 붙여놓은 컨트롤러도 스웨거에서 테스트할 수 있을까요? JWT는 어떻게 받아오죠? 🤔
@Controller('seller')
@ApiTags('Seller API')
@UseGuards(SellerJwtAuthGuard) // seller 로그인 후 접속가능
export class SellerController { ... }
@ApiBearerAuth
보통 JWT 인증에 사용됩니다.
해당 엔드포인트에 대한 요청이 인증되지 않았거나, 유효하지 않은 경우에 엔드포인트에 접근할 수 없도록 합니다.
API 서버는 클라이언트의 요청 헤더에 포함된 JWT를 확인하여 권한을 부여합니다.
먼저 해당 엔드 포인트에 ApiBearerAuth를 추가합니다. 저는 'token'이라는 이름을 부여했습니다.
@Controller('seller')
@ApiBearerAuth('token')
@ApiTags('Seller API')
@UseGuards(SellerJwtAuthGuard) // seller 로그인 후 접속가능
export class SellerController { ... }
ApiBearerAuth이 설정된 엔드포인트들은 아래와 같이 자물쇠 모양이 나타납니다.
이어서 swagger.config.ts를 수정해야 합니다. 앞서 사용한 코드는 아래와 같습니다.
이곳에 인증 옵션을 넣어주면 됩니다.
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export function setupSwagger(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle('Test API Docs') // API 명
.setDescription('NestJS Swagger Test') // API 설명
.setVersion('1.0.0') // 버전
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document); // API 문서 주소
}
bearer 인증에 대한 내용은 `addBearerAuth` 메서드를 이용하면 됩니다.
addBearerAuth(options?: SecuritySchemeObject, name?: string): DocumentBuilder;
SecuritySchemeObject은 아래와 같네요.
export type SecuritySchemeType = 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';
export interface SecuritySchemeObject {
type: SecuritySchemeType;
description?: string;
name?: string;
in?: string;
scheme?: string;
bearerFormat?: string;
flows?: OAuthFlowsObject;
openIdConnectUrl?: string;
'x-tokenName'?: string;
}
저는 JWT를 이용할 것이기 때문에 아래와 같이 작성했습니다.
위와 동일하게 'token'이라는 이름을 부여했습니다. 다르면 작동하지 않으니 주의하셔야 합니다.
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export function setupSwagger(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle('Test API Docs') // API 명
.setDescription('NestJS Swagger Test') // API 설명
.setVersion('1.0.0') // 버전
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'token',
)
.build()
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document); // API 문서 주소
}
Swagger UI 상단에 Authorize 아이콘이 추가된 것을 확인할 수 있습니다.
해당 버튼을 누르면 아래와 같은 화면이 나옵니다.
이곳에 발급된 토큰을 넣어주시면 됩니다.
입력한 JWT는 테스트 요청 시 헤더에 담겨 함께 전송된 후 서버 내부 로직으로 처리되게 됩니다. ✌️
스웨거를 사용하다보면 아시겠지만, 결국 비즈니스 로직 코드 외에 다른 코드들을 추가는 것이다 보니
문서화를 자세히 할수록 코드가 복잡해진다는 단점이 발생합니다.
그럼에도 저에겐 깔끔한 UI와 테스팅의 장점이 더 크게 느껴지는 것 같습니다. 🤔
감사합니다.
[참고자료]
'TS | NestJS' 카테고리의 다른 글
[TS] extends와 implements (0) | 2024.03.22 |
---|---|
[NestJS] Exception filters 추가하기 (feat.Custom Exception) (0) | 2024.03.20 |
[TS] DeepMerge 타입 구현해보기 (0) | 2024.03.06 |
[TS] 타입이 추론되는 String.prototype.split - 2 (0) | 2024.03.05 |
[TS] 타입이 추론되는 String.prototype.split - 1 (0) | 2024.03.03 |