Swagger 문서 자동 생성
ASAPJS는 라우트 데코레이터에 선언된 정보로부터 OpenAPI 3.0 (Swagger) 문서를 자동으로 생성합니다. 별도의 스키마 파일을 작성할 필요 없이, 컨트롤러의 @Get, @Post, @Put, @Delete 데코레이터 옵션과 DTO/TypeIs 선언만으로 완전한 API 문서가 만들어집니다.
동작 원리
Swagger 문서 생성은 세 단계로 이루어집니다:
- 라우트 등록 시점 — 컨트롤러의
registerRoutes()가 호출되면, 각 라우트의excute메서드가IOptions로부터 Swagger path 정보와 스키마를 생성하여addPaths/addScheme으로 등록 - 스키마 수집 —
@asapjs/core의addScheme이 전역 scheme 레지스트리를 관리하고,DocsApplication이 path를 수집 - JSON 생성 — 첫 요청 시
getSwaggerData가 수집된 데이터를 OpenAPI 3.0 형식으로 조합
Swagger 구성 설정
Application.run(config)에 전달하는 설정 객체의 swagger 필드로 Swagger 문서 정보를 구성합니다.
Application.run({
name: 'my-api',
basePath: 'api',
swagger: {
name: 'My API',
version: '1.0.0',
description: 'API 문서 설명',
scheme: 'https',
host: 'api.example.com',
auth_url: '/auth/login',
// 선택: Swagger UI 접근 제어
useAuth: true,
userObject: { admin: 'password123' },
},
// ...
});| 설정 | 타입 | 설명 |
|---|---|---|
name | string | Swagger UI에 표시될 API 이름 |
version | string | API 버전 |
description | string | API 설명 |
scheme | string | URL 스킴 (http 또는 https) |
host | string | API 호스트 도메인 |
auth_url | string | OAuth2 토큰 발급 URL |
useAuth | boolean | Swagger UI에 Basic Auth 보호 적용 여부 |
userObject | object | Basic Auth 사용자 계정 ({ username: password }) |
라우트 데코레이터와 Swagger
IOptions와 Swagger 매핑
라우트 데코레이터의 IOptions 각 필드가 Swagger 문서의 어떤 부분을 생성하는지:
@Post('/register', {
title: '회원 가입', // → summary
description: '새로운 사용자 등록', // → description
deprecated: false, // → deprecated
auth: true, // → 런타임 JWT 미들웨어 제어 (Swagger에는 전역 security 적용)
body: CreateUserDto, // → requestBody schema
bodyContentType: 'application/json', // → requestBody content-type
query: PaginationQueryDto, // → parameters (in: query)
response: UserInfoDto, // → responses.200 schema
})| IOptions 필드 | Swagger 위치 | 설명 |
|---|---|---|
title | summary | 엔드포인트 제목 |
description | description | 상세 설명 |
deprecated | deprecated | 비권장 표시 |
body | requestBody.content.*.schema | 요청 본문 스키마 |
bodyContentType | requestBody.content 키 | Content-Type (application/json 기본) |
query | parameters[] (in: query) | 쿼리 파라미터 |
response | responses.200.content.*.schema | 응답 스키마 |
Path 파라미터 자동 감지
라우트 경로에 :paramName 형태의 Express 파라미터가 있으면, 자동으로 Swagger path 파라미터로 변환됩니다.
// 내부 변환 로직 (packages/router/src/express/router.ts)
// '/posts/:postId' → '/posts/{postId}'
const realPath = swaggerPath
.split('/')
.map((v) => (v.includes(':') ? `{${v.replace(':', '')}}` : v))
.join('/');
// path 파라미터를 Swagger parameters에 추가
swaggerPath.split('/').forEach((v) =>
v.includes(':') &&
parameters.push({
in: 'path',
name: v.replace(':', ''),
required: true,
schema: { type: 'string' },
})
);예를 들어 @Get('/:postId', { ... })는 Swagger에서 다음과 같이 표시됩니다:
parameters:
- in: path
name: postId
required: true
schema:
type: stringDTO → Swagger 스키마 변환
DTO 클래스 기반 스키마
DTO 클래스를 body, query, response에 전달하면, ExtendableDto.swagger() 메서드가 호출되어 $ref 기반 스키마 참조가 생성됩니다.
// ExtendableDto.generateScheme() — 각 필드의 toSwagger()를 호출하여 스키마 생성
{
type: 'object',
properties: {
id: { type: 'integer', format: 'int32', description: 'User ID' },
email: { type: 'string', description: 'Email' },
name: { type: 'string', description: 'Display name' },
}
}
// ExtendableDto.swagger() — $ref 형태로 반환
{ $ref: '#/components/schemas/UserInfoDto' }TypeIs 기반 스키마
DTO 대신 TypeIs.* 함수를 직접 사용할 수도 있습니다:
// 단일 타입
response: TypeIs.BOOLEAN()
// 배열 응답
response: TypeIs.ARRAY(PostInfoDto)
// 페이지네이션 응답
response: TypeIs.PAGING(PostInfoDto)TypeIs.ARRAY와 TypeIs.PAGING은 DTO 클래스 또는 TypeIs.* 함수를 인자로 받아 복합 스키마를 생성합니다. 내부적으로 isClass 검사를 통해 DTO와 TypeIs를 구분합니다.
body 스키마 생성 과정
RouterController의 excute 메서드에서 body 스키마가 생성되는 과정:
// packages/router/src/express/router.ts (간소화)
if (body !== undefined) {
if (isClass(body)) {
// DTO 클래스 → swagger() 호출 → $ref 스키마
const data = new body().swagger();
result.requestBody = this.generateRequestBody(
this.generateDtoScheme(data, 'Body'),
bodyContentType
);
} else {
// TypeIs.* 함수 → toSwagger() 호출
const type = body();
if (['array', 'paging'].includes(type?.__name)) {
result.requestBody = this.generateRequestBody(
this.generateDtoScheme(type?.toSwagger?.(), 'Body'),
bodyContentType
);
} else {
result.requestBody = this.generateRequestBody(
this.generateTypeScheme(body, 'Body'),
bodyContentType
);
}
}
}DocsApplication 내부 구조
Swagger 데이터는 DocsApplication 싱글턴 클래스에서 관리됩니다.
// @asapjs/core — 전역 Swagger 스키마 레지스트리
export function addScheme(data: { name: string; data: any }): void { ... }
export function generateSchemeRefWithName(name: string): string { ... }
// @asapjs/router/src/swagger/index.ts — path 및 최종 JSON 관리
class DocsApplication {
public swaggerData = { ...defaultSwagger }; // OpenAPI 3.0 기본 구조
private swaggerPath = []; // 등록된 path 정보
// 라우트 등록 시 호출
public addPaths = async (data: any) => { ... };
// 첫 요청 시 최종 Swagger JSON 조합 (core의 스키마 레지스트리 사용)
public generateSwaggerData = (req: any) => { ... };
// 캐시된 데이터 반환 (없으면 generate 호출)
public getSwaggerData = (req: any) => { ... };
}
export const { addPaths, getSwaggerData } = new DocsApplication();
export { addScheme, generateSchemeRefWithName } from '@asapjs/core';path 정렬 규칙
Swagger 문서의 엔드포인트는 다음 순서로 정렬됩니다:
tags(컨트롤러의tag값) 기준 알파벳 순- 같은 tag 내에서
path기준 알파벳 순 - 같은 path 내에서 HTTP 메서드 순서:
GET→POST→PUT→DELETE
Swagger UI 접근
기본 엔드포인트
| 엔드포인트 | 설명 |
|---|---|
/{basePath}/docs/swagger-ui.html | Swagger UI 웹 인터페이스 |
/{basePath}/docs/swagger.json | OpenAPI 3.0 JSON 스펙 |
basePath가 api로 설정된 경우:
- Swagger UI:
http://localhost:3000/api/docs/swagger-ui.html - JSON 스펙:
http://localhost:3000/api/docs/swagger.json
Basic Auth 보호
config.swagger.useAuth를 true로 설정하면 Swagger UI에 Basic Auth가 적용됩니다.
// packages/router/src/router/index.ts (간소화)
if (config?.swagger?.useAuth && config?.swagger?.userObject) {
this.app.use(
`/${basePath}/docs/swagger-ui.html`,
basicAuth({
users: config.swagger.userObject, // { admin: 'password' }
challenge: true,
realm: 'Developer',
}),
swaggerUi.serveFiles(undefined, options),
swaggerUi.setup(undefined, options),
);
}useAuth가 설정되지 않으면 경고 로그가 출력됩니다:
@router SWAGGER useAuth is disabled! Please add config.swagger.useAuth & config.swagger.userObject보안 스킴
기본 Swagger 구조에 두 가지 보안 스킴이 포함되어 있습니다:
{
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
},
"OAuthLogin": {
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "/auth/login",
"scopes": {}
}
}
}
}
}
}auth 옵션은 런타임에서 jwtVerification 미들웨어의 적용 여부를 제어합니다. Swagger 문서에는 default-swagger.json에 정의된 전역 security 설정(bearerAuth, OAuthLogin)이 모든 엔드포인트에 동일하게 적용되며, auth 값에 따른 per-endpoint security 구분은 생성되지 않습니다.
완전한 예시
다음은 example 앱의 PostController에서 Swagger 문서가 자동 생성되는 과정입니다:
import { RouterController, Get, Post, Put, Delete, ExecuteArgs } from '@asapjs/router';
import { PaginationQueryDto, TypeIs } from '@asapjs/sequelize';
import CreatePostDto from '../dto/CreatePostDto';
import UpdatePostDto from '../dto/UpdatePostDto';
import PostInfoDto from '../dto/PostInfoDto';
export default class PostController extends RouterController {
public tag = 'Post'; // Swagger 태그 그룹
public basePath = '/posts'; // URL 접두사
constructor() {
super();
this.registerRoutes(); // 여기서 Swagger 스키마도 함께 등록
this.postService = new PostApplication();
}
// Swagger: GET /posts
// - query parameters: page, limit
// - response: PostInfoDto_PAGING 스키마
@Get('/', {
title: '게시글 목록 조회',
description: '게시글 목록을 조회합니다',
query: PaginationQueryDto,
response: TypeIs.PAGING(PostInfoDto),
})
async getPosts({ paging }: ExecuteArgs) { ... }
// Swagger: POST /posts
// - auth: true → 런타임 JWT 미들웨어 적용 (Swagger에는 전역 security)
// - requestBody: CreatePostDto 스키마
// - response: PostInfoDto 스키마
@Post('/', {
title: '게시글 작성',
auth: true,
body: CreatePostDto,
response: PostInfoDto,
})
async createPost({ body, user }: ExecuteArgs) { ... }
// Swagger: GET /posts/{postId}
// - path parameter: postId (자동 감지)
// - response: PostInfoDto 스키마
@Get('/:postId', {
title: '게시글 상세 조회',
response: PostInfoDto,
})
async getPost({ path }: ExecuteArgs) { ... }
// Swagger: DELETE /posts/{postId}
// - auth: true → 런타임 JWT 미들웨어 적용 (Swagger에는 전역 security)
// - path parameter: postId (자동 감지)
@Delete('/:postId', {
title: '게시글 삭제',
auth: true,
})
async deletePost({ path, user }: ExecuteArgs) { ... }
}registerRoutes()가 호출되면 각 데코레이터의 옵션이 처리되어:
addPaths로 4개의 Swagger path 엔트리가 등록됩니다addScheme으로CreatePostDto,PostInfoDto,UpdatePostDto,PostInfoDto_PAGING스키마가 등록됩니다tag: 'Post'로 인해 모든 엔드포인트가 “Post” 그룹으로 묶입니다