NestJS + Supertest 기반의 E2E 테스트로부터 API 문서를 자동 생성하는 문서화 도구
NestJS 애플리케이션의 API 문서화는 일반적으로 **Swagger(OpenAPI)**를 통해 관리됩니다. Swagger는 편리한 UI와 직관적인 문법을 제공하지만, 다음과 같은 문제점이 있습니다.
- 코드 침투 문제: Swagger를 사용하기 위해 프로덕션 코드에 Swagger 어노테이션을 추가해야 합니다. 이는 비즈니스 로직과 API 문서화 코드가 뒤섞이는 문제를 발생시킵니다.
- 문서 최신화 문제: API가 변경되었을 때 Swagger 문서 주석을 업데이트하지 않으면 실제 API와 문서가 일치하지 않게 됩니다. 이는 팀의 혼란과 운영 비용 증가로 이어질 수 있습니다.
NRestDocs는 이 문제를 테스트 기반 문서화 방식으로 해결합니다.
- 프로덕션 코드와 완벽히 분리된 문서화 방식으로 코드의 복잡성을 최소화합니다.
- 이미 작성해야 하는 E2E 테스트를 기반으로 자동으로 문서가 생성되어 항상 최신 상태의 문서를 유지할 수 있습니다.
- 문서와 실제 API가 불일치하면 즉시 테스트가 실패하여, 정확성을 보장합니다.
- 결국 Swagger 문서화와 E2E 테스트를 별도로 작성하는 이중의 비용을 제거하고 효율적으로 관리할 수 있습니다.
다음은 프로덕션 코드에 Swagger 어노테이션이 침투한 예시입니다.
// user.controller.ts
@ApiTags("users")
@Controller("users")
export class UserController {
@ApiOperation({ summary: "사용자 생성" })
@ApiResponse({
status: 201,
description: "생성된 사용자 반환",
type: User,
headers: {
"Set-Cookie": {
description: "세션 쿠키",
schema: { type: "string" },
},
},
})
@Post()
create(@Body() createUserDto: CreateUserDto): User {
return this.userService.create(createUserDto);
}
}- API 명세를 위한 코드와 비즈니스 로직이 혼재됩니다.
- API가 변경되었을 때 Swagger 주석이 누락될 위험이 있습니다.
- 문서의 최신성과 정확성을 보장하는 장치가 없습니다.
NRestDocs를 적용하면, 프로덕션 코드와 완전히 분리된 E2E 테스트에서 문서를 자동 생성할 수 있습니다.
// user.controller.e2e-spec.ts
describe("UserController (e2e)", () => {
it("POST /users - 사용자 생성", async () => {
await docRequest(
request(app.getHttpServer())
.post("/users")
.set("Authorization", "Bearer <token>")
.send({ name: "홍길동", age: 30 })
.expect(201)
)
.withDescription("사용자 생성 API")
.withRequestHeaders([
definedHeader("Authorization")
.type("string")
.description("Bearer 토큰 인증"),
])
.withRequestFields([
definedField("name").type("string").description("사용자 이름"),
definedField("age").type("number").description("사용자 나이"),
])
.withResponseHeaders([
definedHeader("Set-Cookie")
.type("string")
.description("세션 쿠키")
.optional(),
])
.withResponseFields([
definedField("id")
.type("number")
.description("생성된 사용자 ID"),
definedField("name")
.type("string")
.description("생성된 사용자 이름"),
])
.doc("create-user");
});
});docs/create-user/
├── curl-request.adoc
├── http-request.adoc
├── http-response.adoc
├── request-headers.adoc
├── request-fields.adoc
├── response-headers.adoc
└── response-fields.adoc
- 프로덕션 코드를 깨끗하게 유지할 수 있습니다.
- API 변경 시 E2E 테스트도 필수로 변경해야 하므로, 문서 최신화를 자동 보장합니다.
- Strict 모드를 통해 실제 API 응답과 문서 간 불일치가 있으면 즉각적으로 탐지하여 테스트를 실패시킵니다.
- E2E 테스트 코드를 작성하며 문서까지 관리하므로, Swagger와 E2E 테스트를 별도로 관리할 필요가 없어 비용을 효율적으로 관리할 수 있습니다.
| 비교 항목 | Swagger (기존 방식) | NRestDocs (개선된 방식) |
|---|---|---|
| 코드 침투 | 프로덕션 코드에 Swagger 어노테이션 추가 | 프로덕션 코드 영향 없음 |
| 문서 최신화 | 수동 업데이트로 최신성 보장 어려움 | 테스트와 동기화되어 자동 최신화 유지 |
| 정확성 보장 | 문서와 API 불일치 가능성 존재 | Strict 모드로 정확성 자동 보장 |
| 유지보수 비용 | 문서, 테스트 이중 관리로 유지보수 비용 높음 | 테스트 하나로 문서 자동 관리 |
- Jest & Supertest E2E 테스트와 완벽한 통합
- HTTP 요청/응답, 헤더, 파라미터, 필드 문서화 지원
- cURL 명령어를 포함한 상세한 요청 문서 자동 생성
- 엄격한(strict) 모드 지원으로 정확성 보장
- 기본적으로 AsciiDoc 문서 생성 (Markdown 확장 가능)
- 선언적이고 간결한 API 제공
각 메서드는 체이닝으로 이어 호출할 수 있으며, 아래와 같은 구조로 구성되어 있습니다.
await docRequest(request(app.getHttpServer()).get('/path').expect(200))
.withDescription('API 설명')
.withRequestHeaders([...])
.withRequestFields([...])
.withRequestParameters([...])
.withPathParameters([...])
.withRequestParts([...])
.withResponseHeaders([...])
.withResponseFields([...])
.doc('api-identifier');| 인자 | 필수 | 설명 |
|---|---|---|
| desc | ✅ | API에 대한 설명 |
사용 예시
.withDescription('사용자 생성 API')| 필드명 | 필수 | 타입 | 설명 |
|---|---|---|---|
| name | ✅ | string | 헤더 이름 |
| type | ❌ (기본: "string") |
string | 헤더 타입 |
| description | ❌ | string | 헤더 설명 |
| optional | ❌ (기본: false) |
boolean | 필수 여부 |
사용 예시
.withRequestHeaders([
defineHeader('Authorization').description('Bearer 인증 토큰'),
{ name: 'X-Request-ID', description: '요청 추적 ID', optional: true },
])withRequestHeaders와 동일한 구조로 사용합니다.
사용 예시
.withResponseHeaders([
defineHeader('Set-Cookie').description('세션 쿠키'),
])| 필드명 | 필수 | 타입 | 설명 |
|---|---|---|---|
| name | ✅ | string | 필드 이름 |
| type | ✅ | string | 필드 타입 |
| description | ❌ | string | 필드 설명 |
| optional | ❌ (기본: false) |
boolean | 필수 여부 |
사용 예시
.withRequestFields([
defineField('name').type('string').description('사용자 이름'),
defineField('age').type('number').description('사용자 나이').optional(),
])URL 쿼리 파라미터 문서화에 사용됩니다.
| 필드명 | 필수 | 타입 | 설명 |
|---|---|---|---|
| name | ✅ | string | 파라미터 이름 |
| type | ❌ (기본: "string") |
string | 파라미터 타입 |
| description | ❌ | string | 파라미터 설명 |
| optional | ❌ (기본: false) |
boolean | 필수 여부 |
사용 예시
.withRequestParameters([
defineQueryParam('search').description('검색 키워드'),
{ name: 'page', description: '페이지 번호', optional: true },
])URL 경로의 동적 파라미터 문서화에 사용됩니다.
withRequestParameters와 동일한 구조로 사용합니다.
사용 예시
.withPathParameters([
definePathParam('userId').description('사용자 ID'),
])멀티파트 요청 문서화에 사용됩니다. (파일 업로드 등)
| 필드명 | 필수 | 타입 | 설명 |
|---|---|---|---|
| name | ✅ | string | 파트 이름 |
| type | ❌ (기본: "file") |
string | 파트 타입 |
| description | ❌ | string | 파트 설명 |
| optional | ❌ (기본: false) |
boolean | 필수 여부 |
사용 예시
.withRequestParts([
{ name: 'avatar', description: '사용자 아바타 이미지' },
definePart('metadata').type('json').description('부가 정보').optional(),
])마지막으로 호출하는 메서드로, 스니펫 파일을 작성합니다.
| 인자 | 필수 | 설명 |
|---|---|---|
| identifier | ✅ | API를 구별할 고유 식별자 (예: 'create-user') |
사용 예시
.doc('create-user')OpenAPI 서버 URL을 설정합니다.
| 인자 | 필수 | 설명 |
|---|---|---|
| servers | ✅ | 서버 URL 배열 (string[]) |
사용 예시
.withServers([
'http://api.example.com',
'http://api2.example.com'
])HTTP 메서드와 경로를 설정합니다.
| 인자 | 필수 | 설명 |
|---|---|---|
| method | ✅ | HTTP 메서드 |
| path | ✅ | API 경로 |
사용 예시
.withOperation('POST', '/api/users')상태 코드별 응답 정보를 설정합니다.
| 인자 | 필수 | 설명 |
|---|---|---|
| statusCode | ✅ | HTTP 상태 코드 |
| response | ✅ | 응답 정보 객체 |
response 객체 구조
| 필드명 | 필수 | 타입 | 설명 |
|---|---|---|---|
| headers | ❌ | array | 응답 헤더 배열 |
| fields | ❌ | array | 응답 필드 배열 |
| description | ❌ | string | 응답 설명 |
사용 예시
.withResponse(201, {
description: '리소스 생성 성공',
headers: [
defineHeader('Location').description('생성된 리소스 위치')
],
fields: [
defineField('id').type('number').description('생성된 ID'),
defineField('createdAt').type('string').description('생성 일시')
]
})
.withResponse(400, {
description: '잘못된 요청',
fields: [
defineField('error').type('string').description('에러 메시지')
]
})응답 필드가 없는 경우
.withResponse(204, {
description: '리소스 삭제 성공'
})여러 상태 코드의 응답 설정
.withResponse(200, {
description: '성공 응답',
fields: [defineField('result').type('object').description('결과 데이터')]
})
.withResponse(400, {
description: '잘못된 요청',
fields: [defineField('error').type('string').description('에러 메시지')]
})
.withResponse(500, {
description: '서버 오류',
fields: [defineField('message').type('string').description('오류 메시지')]
})- 각 필드나 헤더, 파라미터의 정의를 선언적으로 작성할 수 있는 헬퍼 함수가 제공됩니다.
| 함수 | 기본 type | 설명 |
|---|---|---|
defineHeader(name) |
"string" |
헤더 정의 |
defineField(name) |
필수 입력 | 요청/응답 필드 정의 |
defineQueryParam(name) |
"string" |
쿼리 파라미터 정의 |
definePathParam(name) |
"string" |
경로 파라미터 정의 |
definePart(name) |
"file" |
Multipart 요청 파트 정의 |
defineHeader("Authorization").description("인증 헤더");
defineField("age").type("number").description("사용자 나이");
defineQueryParam("keyword").description("검색 키워드").optional();
definePathParam("userId").description("사용자 ID");
definePart("image").description("프로필 이미지").optional();await docRequest(
request(app.getHttpServer())
.post("/users/:userId/avatar?replace=true")
.set("Authorization", "Bearer token")
.field("description", "프로필 이미지")
.attach("avatar", "./test/avatar.png")
.expect(200)
)
.withDescription("사용자 아바타 업데이트")
.withRequestHeaders([
defineHeader("Authorization").description("Bearer 인증 토큰"),
])
.withPathParameters([definePathParam("userId").description("사용자 ID")])
.withRequestParameters([
defineQueryParam("replace").description("이미지 교체 여부").optional(),
])
.withRequestParts([
definePart("avatar").description("아바타 이미지 파일"),
definePart("description")
.type("string")
.description("이미지 설명")
.optional(),
])
.withResponseHeaders([
defineHeader("Set-Cookie").description("세션 쿠키").optional(),
])
.withResponseFields([
defineField("success").type("boolean").description("성공 여부"),
defineField("url").type("string").description("업로드된 이미지 URL"),
])
.doc("update-user-avatar");npm install --save-dev nrestdocs
# 또는 yarn
yarn add --dev nrestdocsnrestdocs.config.ts를 프로젝트 루트에 생성합니다:
// nrestdocs.config.ts
import { defineConfig } from "nrestdocs";
export default defineConfig({
output: "./docs",
format: "adoc", // or 'md'
strict: true, // 문서와 요청/응답 불일치 시 테스트 실패
});생성된 스니펫을 include하여 하나의 문서로 통합할 수 있습니다:
= 사용자 API 문서
== 사용자 생성 API
=== 요청
include::create-user/curl-request.adoc[]
include::create-user/request-headers.adoc[]
include::create-user/request-fields.adoc[]
=== 응답
include::create-user/http-response.adoc[]
include::create-user/response-headers.adoc[]
include::create-user/response-fields.adoc[]- 기본 AsciiDoc 지원 (Markdown 추가 예정)
- Renderer 및 Writer를 통한 확장 가능
- 테스트와 문서화를 단일 코드로 관리
- HTML, PDF, Notion 등 추가 포맷 지원
- OpenAPI 통합 지원 (Swagger와의 호환성 지원)
이 프로젝트는 오픈소스이며 모든 분들의 기여를 환영합니다. 버그 리포트, 기능 요청, PR 등 언제나 환영합니다!
- GitHub 이슈에 버그 또는 개선사항을 등록해주세요.
- PR은 main 브랜치를 기반으로 작성해주세요.
본 프로젝트는 MIT 라이선스로 배포됩니다.
Copyright (c) Jeong-Rae