8 min read

NestJS Swagger: Complete Setup & API Documentation Guide

Set up NestJS Swagger and document your APIs end-to-end: install @nestjs/swagger, configure DocumentBuilder, master decorators, ship rich OpenAPI specs.

NestJS Swagger: Complete Setup & API Documentation Guide

Auto-generated API documentation is one of the highest-leverage things you can ship in a NestJS service. The @nestjs/swagger package wires your DTOs, controllers, and guards into a live OpenAPI 3 spec that frontend, mobile, and partner teams can read, test, and codegen against. This guide walks the full setup end-to-end: install, bootstrap, decorator reference, two real-world examples, and the pitfalls that bite first-time users.

If you only need a primer on what OpenAPI and Swagger are, start with The OpenAPI specification and Swagger. Otherwise, keep reading.

1. Install and bootstrap

Install the package alongside the Express adapter (use fastify-swagger instead if you run NestJS on Fastify):

npm install @nestjs/swagger swagger-ui-express

Wire SwaggerModule into main.ts. The DocumentBuilder returns an OpenAPI document; SwaggerModule.setup() mounts the interactive UI at the path you choose (commonly /api):

import { NestFactory } from "@nestjs/core"
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"
 
import { AppModule } from "./app.module"
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
 
  const config = new DocumentBuilder()
    .setTitle("Cats API")
    .setDescription("Public REST API for the Cats service")
    .setVersion("1.0.0")
    .addBearerAuth()
    .build()
 
  const document = SwaggerModule.createDocument(app, config)
  SwaggerModule.setup("api", app, document)
 
  await app.listen(3000)
}
 
bootstrap()

Visit http://localhost:3000/api and you'll see the Swagger UI rendered against your live routes.

Turn on the CLI plugin (recommended)

The CLI plugin reads your TypeScript AST at build time and injects @ApiProperty() calls for DTO fields automatically. Without it, every property needs an explicit decorator — tedious and easy to forget. Edit nest-cli.json:

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": ["@nestjs/swagger"]
  }
}

The plugin assumes DTO files end with .dto.ts (or .entity.ts) and that controller files end with .controller.ts. Stick to those suffixes and you'll get the richest spec for free.

2. Decorator reference

Five decorators cover ~90% of real usage. Each one corresponds to a piece of the OpenAPI spec.

@ApiTags

Groups endpoints under a heading in the Swagger UI. Apply it at the controller level so every route under it inherits the tag:

import { Controller, Get } from "@nestjs/common"
import { ApiTags } from "@nestjs/swagger"
 
@Controller("cats")
@ApiTags("cats")
export class CatsController {
  @Get()
  findAll() {
    return []
  }
}

@ApiOperation

Sets the short summary and longer description for a single route. The summary is the one-line label in the UI; the description renders below it as markdown.

import { ApiOperation } from "@nestjs/swagger"
 
@Get(":id")
@ApiOperation({
  summary: "Fetch a cat by id",
  description: "Returns 404 if the cat does not exist.",
})
findOne() { /* ... */ }

@ApiResponse

Describes a possible HTTP response. Repeat it once per status code. The type field tells Swagger which DTO/entity to render in the response schema.

import { ApiResponse } from "@nestjs/swagger"
 
import { CatEntity } from "./cat.entity"
 
@Get(":id")
@ApiResponse({ status: 200, description: "Cat found", type: CatEntity })
@ApiResponse({ status: 404, description: "Cat not found" })
findOne() { /* ... */ }

Tip: @ApiOkResponse(), @ApiCreatedResponse(), @ApiNotFoundResponse(), etc. are sugar over @ApiResponse({ status, ... }) and read better in real codebases.

@ApiProperty

Annotates a single DTO field. With the CLI plugin enabled you only need @ApiProperty() when you want to override the inferred metadata — examples, descriptions, enums, deprecation flags, etc.

import { ApiProperty } from "@nestjs/swagger"
import { IsEmail, IsString, MinLength } from "class-validator"
 
export class CreateCatDto {
  @ApiProperty({ example: "Whiskers" })
  @IsString()
  name: string
 
  @ApiProperty({ description: "Age in human years", example: 3 })
  age: number
 
  @ApiProperty({ format: "email" })
  @IsEmail()
  ownerEmail: string
 
  @ApiProperty({ minLength: 8 })
  @IsString()
  @MinLength(8)
  password: string
}

class-validator decorators (@IsEmail, @MinLength, @Matches) are picked up by the CLI plugin and reflected into the OpenAPI schema — you don't need to repeat them in @ApiProperty.

@ApiBearerAuth

Declares that a route (or controller) requires a Bearer token. Combine with addBearerAuth() in your DocumentBuilder config so the "Authorize" button in Swagger UI lets users paste a token once and have it sent on every "Try it out" request.

import { Controller, Get, UseGuards } from "@nestjs/common"
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"
 
import { JwtAuthGuard } from "../auth/jwt-auth.guard"
 
@Controller("me")
@ApiTags("profile")
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
export class ProfileController {
  @Get()
  current() {
    /* ... */
  }
}

3. Real-world example: CRUD controller

A typical resource controller with all five status codes documented. The CLI plugin handles DTO schema generation; we explicitly document responses so the spec lists every possible outcome.

import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  ParseIntPipe,
  Patch,
  Post,
} from "@nestjs/common"
import {
  ApiCreatedResponse,
  ApiNoContentResponse,
  ApiNotFoundResponse,
  ApiOkResponse,
  ApiOperation,
  ApiTags,
} from "@nestjs/swagger"
 
import { CatsService } from "./cats.service"
import { CreateCatDto } from "./dto/create-cat.dto"
import { UpdateCatDto } from "./dto/update-cat.dto"
import { CatEntity } from "./cat.entity"
 
@Controller("cats")
@ApiTags("cats")
export class CatsController {
  constructor(private readonly cats: CatsService) {}
 
  @Get()
  @ApiOperation({ summary: "List cats" })
  @ApiOkResponse({ type: [CatEntity] })
  findAll() {
    return this.cats.findAll()
  }
 
  @Get(":id")
  @ApiOperation({ summary: "Fetch a cat by id" })
  @ApiOkResponse({ type: CatEntity })
  @ApiNotFoundResponse()
  findOne(@Param("id", ParseIntPipe) id: number) {
    return this.cats.findOne(id)
  }
 
  @Post()
  @ApiOperation({ summary: "Create a cat" })
  @ApiCreatedResponse({ type: CatEntity })
  create(@Body() body: CreateCatDto) {
    return this.cats.create(body)
  }
 
  @Patch(":id")
  @ApiOperation({ summary: "Update a cat" })
  @ApiOkResponse({ type: CatEntity })
  @ApiNotFoundResponse()
  update(@Param("id", ParseIntPipe) id: number, @Body() body: UpdateCatDto) {
    return this.cats.update(id, body)
  }
 
  @Delete(":id")
  @HttpCode(204)
  @ApiOperation({ summary: "Delete a cat" })
  @ApiNoContentResponse()
  @ApiNotFoundResponse()
  remove(@Param("id", ParseIntPipe) id: number) {
    return this.cats.remove(id)
  }
}

For DTO design and validation patterns, see Error handling and data validation.

4. Real-world example: auth-protected endpoint

Pair @ApiBearerAuth with a guard so Swagger UI's "Authorize" button works against your real auth flow:

import { Body, Controller, Post, Req, UseGuards } from "@nestjs/common"
import {
  ApiBearerAuth,
  ApiBody,
  ApiOkResponse,
  ApiOperation,
  ApiTags,
  ApiUnauthorizedResponse,
} from "@nestjs/swagger"
 
import { JwtAuthGuard } from "./jwt-auth.guard"
import { ChangePasswordDto } from "./dto/change-password.dto"
 
@Controller("account")
@ApiTags("account")
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
export class AccountController {
  @Post("change-password")
  @ApiOperation({ summary: "Change the signed-in user's password" })
  @ApiBody({ type: ChangePasswordDto })
  @ApiOkResponse({ description: "Password changed" })
  @ApiUnauthorizedResponse({ description: "Missing or invalid bearer token" })
  changePassword(@Req() req: Request, @Body() body: ChangePasswordDto) {
    /* ... */
  }
}

A complete walkthrough of token-based auth (which provides the JWT this endpoint expects) lives in Authenticating users with bcrypt, Passport, JWT, and cookies.

5. Common pitfalls and FAQ

Circular dependencies between DTOs

If OrderDto references CustomerDto and CustomerDto references OrderDto, Swagger silently emits one of them as {} because the metadata reflection happens before both classes are fully initialized. The fix is to wrap the type in a thunk:

@ApiProperty({ type: () => CustomerDto })
customer: CustomerDto

Use the function form (() => CustomerDto) on both sides and the schema generator will defer evaluation until after the module loads.

File uploads

@nestjs/swagger does not infer multipart bodies. You have to declare them explicitly with @ApiConsumes() and a @ApiBody() block that mirrors the multipart schema:

import { Controller, Post, UploadedFile, UseInterceptors } from "@nestjs/common"
import { FileInterceptor } from "@nestjs/platform-express"
import { ApiBody, ApiConsumes } from "@nestjs/swagger"
 
@Controller("uploads")
export class UploadsController {
  @Post()
  @UseInterceptors(FileInterceptor("file"))
  @ApiConsumes("multipart/form-data")
  @ApiBody({
    schema: {
      type: "object",
      properties: { file: { type: "string", format: "binary" } },
    },
  })
  upload(@UploadedFile() file: Express.Multer.File) {
    return { size: file.size }
  }
}

Without @ApiConsumes, Swagger UI renders the body editor as JSON and your "Try it out" requests fail with a 400.

Enums show up as strings, not dropdowns

Pass enum and enumName to @ApiProperty so the OpenAPI spec emits a named reference rather than an inline string constraint — this is what enables the dropdown in Swagger UI and gives codegen tools a stable type name:

export enum CatColor {
  Black = "black",
  White = "white",
  Tabby = "tabby",
}
 
export class CreateCatDto {
  @ApiProperty({ enum: CatColor, enumName: "CatColor" })
  color: CatColor
}

class-transformer types disappear

If you use @Transform() to coerce a string to a number (e.g. for query params), the CLI plugin infers the original string type. Override it with @ApiProperty({ type: Number }) on the affected field.

The Swagger UI is empty after a fresh install

Almost always one of three things:

  1. SwaggerModule.setup() is called after await app.listen() — flip the order.
  2. The CLI plugin is configured but you skipped nest build / nest start after editing nest-cli.json — restart the dev server.
  3. Controllers aren't registered in AppModule (imports array). The Swagger document only reflects routes Nest knows about.

Next steps

  • Browse the @nestjs/swagger package on NestJS.io for version history, dependency stats, and related packages.
  • Keep iterating on your spec — the moment a frontend or partner consumes it, the cost of a missing @ApiResponse or unnamed enum shows up immediately.
  • When the spec is stable, generate a typed client with openapi-typescript or orval and delete a few hundred lines of hand-written fetch code.
NestJS Swagger: Complete Setup & API Documentation Guide | NestJS.io