--- title: Use API Versioning for Breaking Changes impact: MEDIUM impactDescription: Versioning allows you to evolve APIs without breaking existing clients tags: api, versioning, breaking-changes, compatibility --- ## Use API Versioning for Breaking Changes Use NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints. **Incorrect (breaking changes without versioning):** ```typescript // Breaking changes without versioning @Controller('users') export class UsersController { @Get(':id') async findOne(@Param('id') id: string): Promise { // Original response: { id, name, email } // Later changed to: { id, firstName, lastName, emailAddress } // Old clients break! return this.usersService.findOne(id); } } // Manual versioning in routes @Controller('v1/users') export class UsersV1Controller {} @Controller('v2/users') export class UsersV2Controller {} // Inconsistent, error-prone, hard to maintain ``` **Correct (use NestJS built-in versioning):** ```typescript // Enable versioning in main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); // URI versioning: /v1/users, /v2/users app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1', }); // Or header versioning: X-API-Version: 1 app.enableVersioning({ type: VersioningType.HEADER, header: 'X-API-Version', defaultVersion: '1', }); // Or media type: Accept: application/json;v=1 app.enableVersioning({ type: VersioningType.MEDIA_TYPE, key: 'v=', defaultVersion: '1', }); await app.listen(3000); } // Version-specific controllers @Controller('users') @Version('1') export class UsersV1Controller { @Get(':id') async findOne(@Param('id') id: string): Promise { const user = await this.usersService.findOne(id); // V1 response format return { id: user.id, name: user.name, email: user.email, }; } } @Controller('users') @Version('2') export class UsersV2Controller { @Get(':id') async findOne(@Param('id') id: string): Promise { const user = await this.usersService.findOne(id); // V2 response format with breaking changes return { id: user.id, firstName: user.firstName, lastName: user.lastName, emailAddress: user.email, createdAt: user.createdAt, }; } } // Per-route versioning - different versions for different routes @Controller('users') export class UsersController { @Get() @Version('1') findAllV1(): Promise { return this.usersService.findAllV1(); } @Get() @Version('2') findAllV2(): Promise { return this.usersService.findAllV2(); } @Get(':id') @Version(['1', '2']) // Same handler for multiple versions findOne(@Param('id') id: string): Promise { return this.usersService.findOne(id); } @Post() @Version(VERSION_NEUTRAL) // Available in all versions create(@Body() dto: CreateUserDto): Promise { return this.usersService.create(dto); } } // Shared service with version-specific logic @Injectable() export class UsersService { async findOne(id: string, version: string): Promise { const user = await this.repo.findOne({ where: { id } }); if (version === '1') { return this.toV1Response(user); } return this.toV2Response(user); } private toV1Response(user: User): UserV1Response { return { id: user.id, name: `${user.firstName} ${user.lastName}`, email: user.email, }; } private toV2Response(user: User): UserV2Response { return { id: user.id, firstName: user.firstName, lastName: user.lastName, emailAddress: user.email, createdAt: user.createdAt, }; } } // Controller extracts version @Controller('users') export class UsersController { @Get(':id') async findOne( @Param('id') id: string, @Headers('X-API-Version') version: string = '1', ): Promise { return this.usersService.findOne(id, version); } } // Deprecation strategy - mark old versions as deprecated @Controller('users') @Version('1') @UseInterceptors(DeprecationInterceptor) export class UsersV1Controller { // All V1 routes will include deprecation warning } @Injectable() export class DeprecationInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const response = context.switchToHttp().getResponse(); response.setHeader('Deprecation', 'true'); response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT'); response.setHeader('Link', '; rel="successor-version"'); return next.handle(); } } ``` Reference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning)