Outdated API documentation is worse than no documentation — it actively misleads the developers consuming your API. In a fast-moving ERP project, manually maintaining a Swagger YAML file is impossible: endpoints change, fields get added, validations update. @nestjs/swagger solves this by reading TypeScript decorators and generating an accurate OpenAPI 3.0 spec from your actual code. Every time you add a field to a DTO or a new route to a controller, the docs update automatically. I've used this approach on every NestJS project for the past two years and it's eliminated the documentation drift problem entirely.
@nestjs/swagger uses TypeScript's reflection API (enabled by emitDecoratorMetadata in tsconfig) to read type information from your DTOs and controllers. It combines this with explicit decorators (@ApiProperty, @ApiOperation, @ApiResponse) to build an OpenAPI 3.0 specification. The spec is served as JSON at /api-docs/json and as an interactive Swagger UI at /api-docs. The key insight: without decorators, @nestjs/swagger can infer basic types (string, number) from TypeScript. With decorators and the CLI plugin, it can infer almost everything without manual annotation.
The @nestjs/swagger CLI plugin, enabled in nest-cli.json, automatically adds @ApiProperty decorators to all DTO class properties based on TypeScript types. Without the plugin, you must decorate every DTO field manually. With the plugin, plain class-validator decorated DTOs are automatically documented. The plugin also reads JSDoc comments and adds them as descriptions. Enable it in nest-cli.json under the `plugins` array. After enabling, restart your dev server — documentation for existing DTOs appears immediately without any code changes.
Even with the CLI plugin, some decorators add information the plugin can't infer: @ApiOperation sets the endpoint summary and description (shown in the Swagger UI). @ApiResponse documents specific response codes with example payloads. @ApiTags groups endpoints in the Swagger UI sidebar. @ApiSecurity marks endpoints that require authentication. @ApiQuery and @ApiParam document query parameters and path variables. @ApiBody documents complex request bodies. I use @ApiOperation on every endpoint and @ApiResponse for at least 200, 400, and 401 on authenticated routes.
@nestjs/swagger Documentation Generation Flow:
────────────────────────────────────────────────────────────
TypeScript Source
├── Controllers (@Controller, @Get, @Post, @Version)
│ └── @ApiOperation, @ApiResponse, @ApiTags
├── DTOs (class-validator decorators)
│ └── CLI Plugin auto-adds @ApiProperty
└── Types (TypeScript types + JSDoc)
└── CLI Plugin reads types + comments
│
▼ Swagger module reads metadata at startup
OpenAPI 3.0 JSON spec
│
┌─────────┴──────────┐
▼ ▼
/api-docs /api-docs/json
(Swagger UI) (machine-readable)
│
▼
openapi-generator-cli
│
TypeScript SDK for frontendFrom my experience generating NestJS API docs: enable the `introspectComments` option in the Swagger plugin config to automatically generate field descriptions from JSDoc comments on your DTO properties. Instead of `@ApiProperty({ description: 'The invoice date' })`, just write a JSDoc comment: `/** The invoice date in ISO 8601 format */` above the property. The plugin extracts this and adds it to the OpenAPI spec. This reduces decorator clutter significantly and keeps descriptions next to the code they describe.
The full production setup includes: versioned Swagger documents (one per API version), JWT bearer token configuration so the 'Try it out' feature works with authentication, custom Swagger UI CSS for branding, and environment-based exposure (Swagger is available in development and staging but not in production). I also configure `operationIdFactory` to generate predictable operation IDs from controller method names, which makes generated client SDKs cleaner.
The OpenAPI spec that NestJS generates can be fed into code generators to produce type-safe client SDKs. OpenAPI Generator can generate TypeScript clients for React/Next.js frontends from the spec — run `openapi-generator-cli generate -i http://localhost:3000/api-docs/json -g typescript-axios -o ./src/api`. The generated client has typed request/response interfaces that match your backend DTOs exactly. When you change a DTO field, regenerate the client to catch breaking changes before they reach production. I've used this pattern to keep frontend and backend in sync on several ERP projects.
// main.ts — production Swagger setup
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.enableVersioning({ type: VersioningType.URI })
if (process.env.NODE_ENV !== 'production') {
// V1 documentation
const v1Config = new DocumentBuilder()
.setTitle('ERP API v1')
.setDescription('ERP REST API — v1 (deprecated, migrate to v2)')
.setVersion('1.0')
.addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' })
.addServer('/api/v1')
.build()
const v1Doc = SwaggerModule.createDocument(app, v1Config, {
include: [InvoicesV1Module, CustomersV1Module],
})
SwaggerModule.setup('api-docs/v1', app, v1Doc)
// V2 documentation
const v2Config = new DocumentBuilder()
.setTitle('ERP API v2')
.setDescription('ERP REST API — v2 with cursor pagination and improved schemas')
.setVersion('2.0')
.addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' })
.addServer('/api/v2')
.build()
const v2Doc = SwaggerModule.createDocument(app, v2Config, {
include: [InvoicesV2Module, CustomersV2Module],
})
SwaggerModule.setup('api-docs/v2', app, v2Doc)
}
await app.listen(3000)
}
// nest-cli.json — enable Swagger CLI plugin
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [{
"name": "@nestjs/swagger",
"options": {
"introspectComments": true, // JSDoc → descriptions
"classValidatorShim": true // class-validator → required/optional
}
}]
}
}
// create-invoice.dto.ts — with JSDoc (plugin reads these)
export class CreateInvoiceDto {
/** The invoice number (auto-generated if omitted) */
@IsOptional()
@IsString()
invoiceNumber?: string
/** Total invoice amount in IDR */
@IsNumber()
@Min(0)
amount: number
/** Customer ID this invoice belongs to */
@IsUUID()
customerId: string
}JWT authentication is documented with `addBearerAuth()` on the DocumentBuilder and `@ApiBearerAuth()` on protected controllers or routes. This adds an 'Authorize' button to the Swagger UI where testers can paste their JWT token. All subsequent 'Try it out' requests will include the Authorization header automatically. For multi-scheme authentication (JWT for users, API key for service accounts), add both schemes with `addBearerAuth` and `addApiKey` and annotate endpoints accordingly.
The Swagger UI at /api-docs is a complete map of your API surface area — all endpoints, all parameters, all response schemas. Leaving it publicly accessible in production gives attackers a detailed guide to your API. At minimum, put it behind basic authentication or IP allowlisting. Better: expose it only in non-production environments by checking process.env.NODE_ENV. I use a middleware guard on the /api-docs path that returns 404 in production and a login prompt in staging — only developers with staging credentials can access the docs.
The biggest risk for documentation drift is when DTO validation (class-validator) and OpenAPI documentation diverge. A field might be @IsOptional() in validation but not marked optional in the OpenAPI spec, misleading clients. The fix: use the NestJS mapped types (@nestjs/mapped-types) to extend DTOs. `PartialType(CreateInvoiceDto)` creates an UpdateInvoiceDto where all fields are optional — and the @nestjs/swagger version of PartialType (`from @nestjs/swagger`) additionally marks them as not required in the OpenAPI spec. Always import mapped types from @nestjs/swagger, not @nestjs/mapped-types, when Swagger is enabled.
To prevent documentation regressions from being deployed, validate the generated OpenAPI spec in CI. On every PR, generate the spec with a script (`ts-node generate-spec.ts`) and run it through an OpenAPI validator (`swagger-cli validate api-docs.json`). Better: run a diff between the PR's spec and the main branch spec, and fail the PR if breaking changes are detected without a version bump. Tools like openapi-diff can detect breaking changes (removed endpoints, changed required fields) vs non-breaking changes (added optional fields, new endpoints).