On this page

API documentation with Swagger and OpenAPI

12 min read TextCh. 5 — Production

Swagger and OpenAPI

Swagger (now called the OpenAPI specification) is the industry standard for documenting REST APIs. NestJS provides the @nestjs/swagger package which integrates deeply with the framework — it reads your decorators, DTOs, and type information to generate interactive API documentation automatically.

Install the package:

npm install @nestjs/swagger

Setting up Swagger

Initialize Swagger in main.ts using DocumentBuilder and SwaggerModule:

const config = new DocumentBuilder()
  .setTitle('Bookstore API')
  .setVersion('1.0')
  .setDescription('The bookstore API description')
  .setContact('David Morales', 'https://moralesvegadavid.com', '[email protected]')
  .setLicense('MIT', 'https://opensource.org/licenses/MIT')
  .addServer('http://localhost:3000', 'Development server')
  .addServer('https://api.bookstore.com', 'Production server')
  .addBearerAuth()
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);

Visit http://localhost:3000/api/docs to see the interactive UI.

Decorating controllers

@ApiTags

Group endpoints by tag:

@ApiTags('books')
@Controller('books')
export class BooksController {}

@ApiOperation

Describe what a route does:

@Get(':id')
@ApiOperation({
  summary: 'Get a book by ID',
  description: 'Returns a single book with author and category details.',
})
findOne(@Param('id', ParseIntPipe) id: number) {}

@ApiResponse

Document possible responses:

@Get(':id')
@ApiResponse({ status: 200, description: 'Book found', type: BookResponseDto })
@ApiResponse({ status: 404, description: 'Book not found' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
findOne(@Param('id', ParseIntPipe) id: number) {}

@ApiParam and @ApiQuery

Document path and query parameters:

@Get(':id')
@ApiParam({ name: 'id', type: Number, description: 'Book ID', example: 1 })
findOne(@Param('id', ParseIntPipe) id: number) {}

@Get()
@ApiQuery({ name: 'page', type: Number, required: false, example: 1 })
@ApiQuery({ name: 'search', type: String, required: false, example: 'clean code' })
findAll() {}

Decorating DTOs

@ApiProperty

Document DTO fields:

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateBookDto {
  @ApiProperty({ example: 'Clean Code', maxLength: 200 })
  title: string;

  @ApiProperty({ example: 39.99, minimum: 0 })
  price: number;

  @ApiPropertyOptional({ example: 1 })
  categoryId?: number;
}

Enum properties

enum BookFormat { HARDCOVER = 'hardcover', PAPERBACK = 'paperback', EBOOK = 'ebook' }

export class CreateBookDto {
  @ApiProperty({ enum: BookFormat, default: BookFormat.PAPERBACK })
  format: BookFormat;
}

Array properties

export class CreateBookDto {
  @ApiProperty({ type: [String], example: ['fiction', 'adventure'] })
  tags: string[];
}

Nested DTOs

export class OrderDto {
  @ApiProperty({ type: [OrderItemDto] })
  @ValidateNested({ each: true })
  @Type(() => OrderItemDto)
  items: OrderItemDto[];
}

Response DTOs

Define separate response DTOs that include computed fields:

export class BookResponseDto {
  @ApiProperty({ example: 1 })
  id: number;

  @ApiProperty({ example: 'Clean Code' })
  title: string;

  @ApiProperty({ example: 39.99 })
  price: number;

  @ApiProperty({ type: AuthorSummaryDto })
  author: AuthorSummaryDto;

  @ApiProperty({ example: '2026-04-02T00:00:00.000Z' })
  createdAt: string;
}

Use ClassSerializerInterceptor with @Exclude() and @Expose() from class-transformer to control what properties are included in responses.

Authentication in Swagger UI

Configure the Authorize button for JWT:

const config = new DocumentBuilder()
  .addBearerAuth(
    {
      type: 'http',
      scheme: 'bearer',
      bearerFormat: 'JWT',
      name: 'Authorization',
      in: 'header',
    },
    'access-token', // Reference name
  )
  .build();

Apply to controllers that require authentication:

@ApiBearerAuth('access-token')
@Controller('books')
export class BooksController {}

With persistAuthorization: true in the setup options, the Swagger UI remembers the token between page refreshes.

Swagger CLI plugin

Add the plugin to nest-cli.json to avoid manually decorating every DTO field:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/swagger",
        "options": {
          "introspectComments": true,
          "classValidatorShim": true
        }
      }
    ]
  }
}

The plugin reads TypeScript types and JSDoc comments to generate @ApiProperty metadata automatically. You still add explicit @ApiProperty for examples and special cases, but the boilerplate is eliminated.

Exporting the OpenAPI document

Generate the OpenAPI JSON at build time for client generation:

// generate-swagger.ts (run as a separate script)
async function generateOpenApiSpec(): Promise<void> {
  const app = await NestFactory.create(AppModule, { logger: false });
  const config = new DocumentBuilder()
    .setTitle('Bookstore API')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, config);

  writeFileSync('./openapi.json', JSON.stringify(document, null, 2));
  await app.close();
}

generateOpenApiSpec();

Then use orval or openapi-generator to generate a TypeScript client from openapi.json.

Swagger CLI plugin
Add the @nestjs/swagger plugin to nest-cli.json to avoid writing @ApiProperty on every DTO field. The plugin reads TypeScript types at build time and generates the API property metadata automatically. You only need @ApiProperty for examples, descriptions, and other custom metadata.
Export OpenAPI JSON for client generation
Save the OpenAPI JSON document from SwaggerModule.createDocument() to a file during the build step. Use it with openapi-generator or orval to automatically generate type-safe client SDKs for your frontend in TypeScript, Angular, React, or other languages.
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

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

  app.useGlobalPipes(
    new ValidationPipe({ whitelist: true, transform: true }),
  );

  // Only expose Swagger in non-production environments
  if (process.env['NODE_ENV'] !== 'production') {
    const config = new DocumentBuilder()
      .setTitle('Bookstore API')
      .setDescription('Complete REST API for the bookstore application')
      .setVersion('1.0')
      .addBearerAuth(
        { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
        'access-token',
      )
      .addTag('books', 'Book management endpoints')
      .addTag('authors', 'Author management endpoints')
      .addTag('auth', 'Authentication and authorization')
      .build();

    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('api/docs', app, document, {
      swaggerOptions: { persistAuthorization: true },
    });
  }

  await app.listen(3000);
}

bootstrap();