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.
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-expressWire 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: CustomerDtoUse 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:
SwaggerModule.setup()is called afterawait app.listen()— flip the order.- The CLI plugin is configured but you skipped
nest build/nest startafter editingnest-cli.json— restart the dev server. - Controllers aren't registered in
AppModule(importsarray). The Swagger document only reflects routes Nest knows about.
Next steps
- Browse the
@nestjs/swaggerpackage 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
@ApiResponseor unnamed enum shows up immediately. - When the spec is stable, generate a typed client with
openapi-typescriptororvaland delete a few hundred lines of hand-written fetch code.