NestJS API Versioning이란?
API가 진화하면 기존 클라이언트와의 호환성을 유지하면서 새로운 기능을 제공해야 합니다. NestJS API Versioning은 프레임워크 레벨에서 버전 관리를 지원하여, URI 경로, 헤더, 미디어 타입, 커스텀 로직 등 다양한 전략으로 API 버전을 분리할 수 있습니다. v10+부터 내장 지원되며, 별도 라이브러리 없이 선언적으로 버전별 엔드포인트를 관리합니다.
버전 관리 전략 4가지
NestJS는 네 가지 버전 관리 전략을 제공합니다. main.ts에서 글로벌로 설정합니다.
import { VersioningType } from '@nestjs/common';
// 1. URI 방식 (가장 일반적): /v1/users, /v2/users
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1', // 기본 버전
prefix: 'v', // 접두사 (기본: 'v')
});
// 2. Header 방식: X-API-Version: 1
app.enableVersioning({
type: VersioningType.HEADER,
header: 'X-API-Version',
});
// 3. Media Type 방식: Accept: application/json;v=1
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'v=',
});
// 4. Custom 방식: 요청에서 버전을 추출하는 커스텀 로직
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor: (request: Request) => {
// 쿼리 파라미터에서 버전 추출: ?api-version=2
const version = request.query['api-version'] as string;
return version || '1';
},
});
| 전략 | 요청 예시 | 장점 | 단점 |
|---|---|---|---|
| URI | GET /v2/users |
직관적, 캐싱 용이 | URL이 변경됨 |
| Header | X-API-Version: 2 |
URL 깔끔 | 브라우저 테스트 불편 |
| Media Type | Accept: app/json;v=2 |
REST 표준 준수 | 복잡한 Accept 파싱 |
| Custom | 자유 정의 | 완전한 유연성 | 직접 구현 필요 |
컨트롤러·메서드 레벨 버전 설정
@Version() 데코레이터로 컨트롤러 전체 또는 개별 메서드에 버전을 지정합니다.
// 컨트롤러 레벨 버전: 모든 엔드포인트가 v1
@Controller('users')
@Version('1')
export class UsersV1Controller {
@Get()
findAll() {
return { version: 1, data: ['user1', 'user2'] };
}
@Get(':id')
findOne(@Param('id') id: string) {
return { version: 1, id, name: 'User V1' };
}
}
// v2 컨트롤러: 응답 구조 변경
@Controller('users')
@Version('2')
export class UsersV2Controller {
@Get()
findAll() {
return {
version: 2,
data: [{ id: 1, name: 'user1', email: 'user1@test.com' }],
meta: { total: 1, page: 1 },
};
}
}
// 메서드 레벨 버전: 컨트롤러 내에서 혼합
@Controller('products')
export class ProductsController {
// v1과 v2 모두 응답 (VERSION_NEUTRAL과 유사)
@Get()
@Version(['1', '2'])
findAll() {
return { products: [] };
}
// v2에서만 추가된 엔드포인트
@Get('trending')
@Version('2')
getTrending() {
return { trending: [] };
}
}
VERSION_NEUTRAL: 버전 무관 엔드포인트
헬스체크, 인증 등 모든 버전에서 동일하게 동작해야 하는 엔드포인트는 VERSION_NEUTRAL을 사용합니다.
import { VERSION_NEUTRAL } from '@nestjs/common';
// 모든 버전에서 접근 가능
@Controller('health')
@Version(VERSION_NEUTRAL)
export class HealthController {
@Get()
check() {
return { status: 'ok' };
}
}
// /v1/health ✅
// /v2/health ✅
// /health ✅ (URI 방식에서 prefix 없이도 접근)
// 인증 엔드포인트도 버전 중립
@Controller('auth')
@Version(VERSION_NEUTRAL)
export class AuthController {
@Post('login')
login(@Body() dto: LoginDto) { /* ... */ }
@Post('refresh')
refresh(@Body() dto: RefreshDto) { /* ... */ }
}
서비스 계층 버전 분리 패턴
컨트롤러뿐 아니라 서비스 계층도 버전별로 분리하면 비즈니스 로직 변경을 깔끔하게 관리할 수 있습니다. Dynamic Module 패턴과 조합하면 더 유연해집니다.
// 서비스 인터페이스
export interface UserService {
findAll(): Promise<any>;
findOne(id: number): Promise<any>;
}
// V1 서비스
@Injectable()
export class UserServiceV1 implements UserService {
async findAll() {
return this.userRepo.find();
}
async findOne(id: number) {
return this.userRepo.findOne({ where: { id } });
}
}
// V2 서비스: 페이지네이션, 캐싱 추가
@Injectable()
export class UserServiceV2 implements UserService {
async findAll() {
const [data, total] = await this.userRepo.findAndCount();
return { data, meta: { total } };
}
async findOne(id: number) {
const cached = await this.cache.get(`user:${id}`);
if (cached) return cached;
const user = await this.userRepo.findOne({
where: { id },
relations: ['profile', 'roles'],
});
await this.cache.set(`user:${id}`, user, 300);
return user;
}
}
// 모듈에서 버전별 서비스 등록
@Module({
controllers: [UsersV1Controller, UsersV2Controller],
providers: [
{ provide: 'USER_SERVICE_V1', useClass: UserServiceV1 },
{ provide: 'USER_SERVICE_V2', useClass: UserServiceV2 },
],
})
export class UsersModule {}
// 컨트롤러에서 주입
@Controller('users')
@Version('2')
export class UsersV2Controller {
constructor(
@Inject('USER_SERVICE_V2')
private readonly userService: UserService,
) {}
@Get()
findAll() {
return this.userService.findAll();
}
}
버전 Deprecation 전략
오래된 API 버전을 점진적으로 폐기하는 패턴입니다. Interceptor를 활용하여 deprecated 버전에 경고 헤더를 추가합니다.
// Deprecation 인터셉터
@Injectable()
export class DeprecationInterceptor implements NestInterceptor {
private readonly deprecatedVersions: Map<string, string> = new Map([
['1', '2025-12-31'], // v1은 2025년 말 종료
]);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
// 요청된 API 버전 추출
const version = request.version || request.headers['x-api-version'];
if (version && this.deprecatedVersions.has(version)) {
const sunset = this.deprecatedVersions.get(version);
response.setHeader('Deprecation', 'true');
response.setHeader('Sunset', sunset);
response.setHeader(
'Link',
'</v2/docs>; rel="successor-version"',
);
response.setHeader(
'X-Deprecation-Notice',
`API v${version} is deprecated. Migrate to v2 before ${sunset}.`,
);
}
return next.handle();
}
}
// 글로벌 적용
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: DeprecationInterceptor },
],
})
export class AppModule {}
// 응답 헤더 예시:
// Deprecation: true
// Sunset: 2025-12-31
// Link: </v2/docs>; rel="successor-version"
// X-Deprecation-Notice: API v1 is deprecated. Migrate to v2 before 2025-12-31.
Swagger 버전별 문서 분리
OpenAPI(Swagger) 문서를 버전별로 분리하면 클라이언트 개발자가 정확한 API 스펙을 확인할 수 있습니다.
// main.ts: 버전별 Swagger 문서 생성
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' });
// V1 문서
const v1Config = new DocumentBuilder()
.setTitle('API V1')
.setVersion('1.0')
.addTag('users-v1')
.build();
const v1Document = SwaggerModule.createDocument(app, v1Config, {
include: [UsersV1Module, ProductsV1Module],
});
SwaggerModule.setup('docs/v1', app, v1Document);
// V2 문서
const v2Config = new DocumentBuilder()
.setTitle('API V2')
.setVersion('2.0')
.addTag('users-v2')
.build();
const v2Document = SwaggerModule.createDocument(app, v2Config, {
include: [UsersV2Module, ProductsV2Module],
});
SwaggerModule.setup('docs/v2', app, v2Document);
await app.listen(3000);
}
// /docs/v1 → V1 API 문서
// /docs/v2 → V2 API 문서
운영 베스트 프랙티스
- URI 방식 권장: 가장 직관적이고 캐싱/로깅/모니터링 도구와 호환성이 좋습니다
- 최대 2~3 버전 유지: 동시 지원 버전이 많으면 유지보수 비용이 급증합니다
- VERSION_NEUTRAL 활용: 인증, 헬스체크 등 공통 엔드포인트는 버전 중립으로 설정하세요
- Deprecation 헤더 필수: 폐기 예정 버전에 Sunset 헤더를 추가하여 클라이언트에 사전 고지하세요
- 서비스 계층 분리: 컨트롤러만 분리하면 서비스 코드가 분기문으로 복잡해집니다 — 서비스도 버전별로 분리하세요
- 버전별 E2E 테스트: 각 버전의 엔드포인트가 독립적으로 동작하는지 E2E 테스트로 검증하세요