NestJS API Versioning with Swagger

+15 Mana ✨

Introduction

When your API has multiple versions, documentation should reflect each version's specifics. Swagger can generate separate documentation for each version, helping developers understand what's available in each.

Key Concepts

  • Multi-Document Setup: Separate Swagger docs per version
  • Version Tags: Group endpoints by version
  • Deprecation Markers: Show deprecated endpoints
  • Version Comparison: Help clients see differences

Real World Context

Versioned documentation helps:

  • Developers using specific versions
  • Migration planning
  • API exploration
  • Support team troubleshooting

Deep Dive

Multiple Swagger Documents

Generate separate Swagger documents per major version by filtering modules with the include option.

typescript
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableVersioning({
    type: VersioningType.URI,
  });

  // V1 Documentation
  const v1Config = new DocumentBuilder()
    .setTitle('API v1')
    .setDescription('Version 1 of the API (Deprecated)')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const v1Document = SwaggerModule.createDocument(app, v1Config, {
    include: [UsersV1Module, ProductsV1Module],
  });
  SwaggerModule.setup('api/v1/docs', app, v1Document);

  // V2 Documentation
  const v2Config = new DocumentBuilder()
    .setTitle('API v2')
    .setDescription('Version 2 of the API (Current)')
    .setVersion('2.0')
    .addBearerAuth()
    .build();
  const v2Document = SwaggerModule.createDocument(app, v2Config, {
    include: [UsersV2Module, ProductsV2Module],
  });
  SwaggerModule.setup('api/v2/docs', app, v2Document);

  await app.listen(3000);
}

Each document is mounted at a different URL path (/api/v1/docs vs /api/v2/docs) for clear separation.

Version Tags in Single Document

If you prefer a single Swagger document, use version-prefixed tags to group endpoints visually.

typescript
const config = new DocumentBuilder()
  .setTitle('API')
  .addTag('v1-users', 'Users API v1 (deprecated)')
  .addTag('v2-users', 'Users API v2')
  .build();

@Controller({ path: 'users', version: '1' })
@ApiTags('v1-users')
export class UsersV1Controller { ... }

@Controller({ path: 'users', version: '2' })
@ApiTags('v2-users')
export class UsersV2Controller { ... }

Tag descriptions appear in the Swagger UI sidebar, making it easy to identify deprecated groups.

Deprecation in Swagger

Set deprecated: true in @ApiOperation() to render the endpoint with a strikethrough in Swagger UI.

typescript
@Controller('users')
export class UsersController {
  @Get()
  @Version('1')
  @ApiOperation({
    summary: 'Get all users',
    deprecated: true,
    description: 'DEPRECATED: Use /v2/users instead. Will be removed on 2025-12-31.',
  })
  findAllV1() { ... }

  @Get()
  @Version('2')
  @ApiOperation({ summary: 'Get all users with pagination' })
  findAllV2() { ... }
}

The description field is a good place to include the sunset date and a direct link to the replacement endpoint.

Schema Versioning

Register both DTO versions as extra models and reference the correct one per versioned endpoint.

typescript
// Register both DTO versions
const document = SwaggerModule.createDocument(app, config, {
  extraModels: [UserV1Dto, UserV2Dto],
});

// Reference specific version
@Get()
@Version('1')
@ApiOkResponse({ type: UserV1Dto })
findAllV1() { ... }

@Get()
@Version('2')
@ApiOkResponse({ type: UserV2Dto })
findAllV2() { ... }

This ensures each version's documentation shows the exact response shape clients should expect.

Documentation Landing Page

Provide a root endpoint that lists all available API versions, their status, and documentation URLs.

typescript
@Controller()
export class ApiDocsController {
  @Get()
  @ApiExcludeEndpoint()
  redirectToDocs(@Res() res: Response) {
    res.redirect('/api/v2/docs');
  }
}

// Or create a landing page
@Controller('api')
export class ApiVersionsController {
  @Get()
  getVersions() {
    return {
      versions: [
        {
          version: 'v1',
          status: 'deprecated',
          docsUrl: '/api/v1/docs',
          sunsetDate: '2025-12-31',
        },
        {
          version: 'v2',
          status: 'current',
          docsUrl: '/api/v2/docs',
        },
      ],
      latest: 'v2',
    };
  }
}

The landing page doubles as a discovery endpoint for automated tools that need to find the latest API version.

Version-Specific Examples

Add example values to @ApiProperty() that reflect each version's actual response format.

typescript
export class UserV1Dto {
  @ApiProperty({ example: '123' })
  id: string;

  @ApiProperty({ example: 'John Doe' })
  name: string;
}

export class UserV2Dto {
  @ApiProperty({ example: '123' })
  id: string;

  @ApiProperty({ example: 'John' })
  firstName: string;

  @ApiProperty({ example: 'Doe' })
  lastName: string;
}

Version-specific examples make the migration path obvious when developers compare v1 and v2 side by side.

Common Pitfalls

  1. Single doc for all versions: Confusing when schemas differ.
  2. No deprecation markers: Users don't know what's outdated.
  3. Missing examples: Version differences unclear without them.

Best Practices

  • Create separate Swagger documents per major version
  • Mark deprecated endpoints clearly
  • Include examples showing version differences
  • Provide a landing page listing all versions
  • Link to migration guides from deprecated endpoints

Summary

Versioned Swagger documentation uses multiple documents or tags for different versions. Mark deprecated endpoints, include version-specific examples, and provide a landing page listing available versions with their status.

Code Examples

typescript
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableVersioning({
    type: VersioningType.URI,
  });

  // V1 Documentation
  const v1Config = new DocumentBuilder()
    .setTitle('API v1')
    .setDescription('Version 1 of the API (Deprecated)')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const v1Document = SwaggerModule.createDocument(app, v1Config, {
    include: [UsersV1Module, ProductsV1Module],
  });
  SwaggerModule.setup('api/v1/docs', app, v1Document);

  // V2 Documentation
  const v2Config = new DocumentBuilder()
    .setTitle('API v2')
    .setDescription('Version 2 of the API (Current)')
    .setVersion('2.0')
    .addBearerAuth()
    .build();
  const v2Document = SwaggerModule.createDocument(app, v2Config, {
    include: [UsersV2Module, ProductsV2Module],
  });
  SwaggerModule.setup('api/v2/docs', app, v2Document);

  await app.listen(3000);
}
✓ Completed