Intergrating Swagger To A Nestjs application: Build API Documentation That Actually Gets Used
Learn how to set up interactive Swagger documentation in NestJS that your team will actually use. Live examples, authentication setup, and production-ready patterns in 20 minutes.
Swagger Documentation in NestJS: A Friendly Guide
What you'll learn
By the end of this guide, you'll have live API documentation running at http://localhost:3000/docs
that your team can actually use. No more "check the code" responses.
Who this is for
- Backend Engineers who are tired of answering the same API questions repeatedly
- Full-Stack Developers building APIs that frontend teams need to consume
- Tech Leads who want to improve team collaboration and reduce documentation debt
- You - if you've built a few NestJS endpoints and want to document them properly without losing your mind
Try Kodaschool for free
Click below to sign up and get access to free web, android and iOs challenges.
Prerequisites
Essential Knowledge:
- Basic TypeScript (interfaces, classes, decorators)
- NestJS fundamentals (controllers, services, modules)
- Experience building at least 2-3 REST endpoints
- Familiarity with DTOs (Data Transfer Objects)
What You Should Have Running:
- Node.js (v16 or higher)
- A NestJS project (we'll start from scratch so dont worry if you dont have one)
- A code editor
Wait, what even is Swagger?
Let's start with the problem. You build an API. Your teammates need to use it. They ask you on Slack: "What's the endpoint? What parameters? What comes back?" You copy-paste code snippets. They ask more questions. This happens ten times a day.

Swagger (now called OpenAPI) solves this. It's a live, interactive documentation page that shows:
- Every endpoint you have
- What data to send (with examples!)
- What you get back
- A "try it out" button so people can test without Postman
Think of it as your API's instruction manual that updates itself as you code. When you add @ApiProperty()
to your code, Swagger reads it and shows it in a nice UI. Your teammates can click around, see examples, and test endpoints right there in their browser.
The best part? Once it's set up, maintaining it is just decorating your code as you go. No separate doc files to remember updating.
Before you begin
You need Node.js installed and a NestJS project. If you don't have one yet, create it:
npm i -g @nestjs/cli
nest new tasks-api
Here is what hould be seeing:

Then install Swagger:
npm install @nestjs/swagger swagger-ui-express
That's it. No complex config files yet.
Step 1: Turn on Swagger in your app
Open src/main.ts
. Right now it probably looks like this:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
Add these lines to the main.ts
file:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// This is your Swagger config
const config = new DocumentBuilder()
.setTitle('Tasks API')
.setDescription('Simple task management API')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
await app.listen(3000);
}
bootstrap();
What just happened? You told NestJS to create a documentation page at /docs
. The DocumentBuilder
is where you set the title and description people see when they open your docs. The .addBearerAuth()
part adds an "Authorize" button (we'll use this later for protected endpoints).
Run your app:
npm run start:dev
Open http://localhost:3000/docs
in your browser.
You should see the Swagger UI! It's probably empty right now because we haven't documented any endpoints yet, but the page is there. This is your API documentation home. Bookmark it, because you'll be here a lot.
Step 2: Make your data types visible
Here's where it gets interesting. Swagger can't read your mind. If you have a DTO like this:
export class CreateTaskDto {
title: string;
description?: string;
}
Swagger sees it as an empty object. Useless. Your teammates won't know what fields to send.
You need to decorate each field so Swagger knows what to show. Create src/tasks/dto/create-task.dto.ts
:
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsOptional } from 'class-validator';
export class CreateTaskDto {
@ApiProperty({
description: 'Task title',
example: 'Buy groceries'
})
@IsNotEmpty()
@IsString()
title: string;
@ApiProperty({
description: 'Task description',
example: 'Milk, eggs, bread',
required: false
})
@IsOptional()
@IsString()
description?: string;
}
See those @ApiProperty()
decorators? That's Swagger's magic word. Now when someone looks at your docs, they see:
- Field names (title, description)
- What each field is for
- Example values they can copy-paste
- Which fields are optional
The example
is especially important. When someone clicks "Try it out" in Swagger, these examples pre-fill the form. They don't have to guess what "title" should look like.
Step 3: Document what your endpoints do
Open your controller (or create src/tasks/tasks.controller.ts
):
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateTaskDto } from './dto/create-task.dto';
@Controller('tasks')
export class TasksController {
@Post()
create(@Body() createTaskDto: CreateTaskDto) {
return {
id: '123',
...createTaskDto,
createdAt: new Date()
};
}
@Get()
findAll() {
return [
{ id: '1', title: 'Task 1', description: 'Do something' },
{ id: '2', title: 'Task 2', description: 'Do another thing' }
];
}
}
This works, but Swagger doesn't know what these endpoints do. Let's fix that:
import {
Controller,
Get,
Post,
Body
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth
} from '@nestjs/swagger';
import { CreateTaskDto } from './dto/create-task.dto';
@ApiTags('tasks')
@ApiBearerAuth()
@Controller('tasks')
export class TasksController {
@Post()
@ApiOperation({ summary: 'Create a new task' })
@ApiResponse({
status: 201,
description: 'Task created successfully',
type: CreateTaskDto
})
@ApiResponse({
status: 400,
description: 'Invalid input'
})
create(@Body() createTaskDto: CreateTaskDto) {
return {
id: '123',
...createTaskDto,
createdAt: new Date()
};
}
@Get()
@ApiOperation({ summary: 'Get all tasks' })
@ApiResponse({
status: 200,
description: 'Returns all tasks',
type: [CreateTaskDto]
})
findAll() {
return [
{ id: '1', title: 'Task 1', description: 'Do something' },
{ id: '2', title: 'Task 2', description: 'Do another thing' }
];
}
}
Let me explain what each decorator does:
@ApiTags('tasks')
at the top groups all these endpoints under a "tasks" section in Swagger. If you have 50 endpoints, this grouping makes them scannable instead of overwhelming.
@ApiOperation({ summary: 'Create a new task' })
adds a short description under each endpoint. When someone's scanning your docs, they see "Create a new task" instead of just "POST /tasks" and immediately know what it does.
@ApiResponse()
shows what comes back. The status: 201
tells Swagger "this is a success response." The type: CreateTaskDto
tells it "the response looks like this DTO." If you put type: [CreateTaskDto]
with brackets, Swagger knows it's an array.
@ApiBearerAuth()
at the controller level adds a little lock icon to all these endpoints. It tells users "you need to be authenticated to use these."
Refresh http://localhost:3000/docs
. Your endpoints are now organized, described, and documented. Click "Try it out" on the POST endpoint and you'll see your example values pre-filled!
Step 4: Make the "Authorize" button actually work
Right now that lock icon and "Authorize" button don't do anything. Let's fix that with a simple auth guard.
Create src/auth/auth.guard.ts
:
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException
} from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization;
// Simple check for demo - replace with real JWT validation
if (token !== 'Bearer secret-token') {
throw new UnauthorizedException('Invalid token');
}
return true;
}
}
In your controller, add the guard:
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
@UseGuards(AuthGuard)
@ApiTags('tasks')
@ApiBearerAuth()
@Controller('tasks')
export class TasksController {
// ... your endpoints
}
Now go back to Swagger. Click the "Authorize" button at the top right. You'll see a modal asking for a token. Type secret-token
(without "Bearer", Swagger adds that) and click "Authorize."
Try calling an endpoint now. It works! Without the token, you get a 401 error. With it, you get your response.
This is huge for your team. They can test authenticated endpoints right in the browser without setting up Postman, configuring auth, and copying tokens around.
Step 5: Show accurate response types
Your create
endpoint returns an object with id
and createdAt
, but Swagger only shows the input DTO. That's confusing. Let's create a proper response DTO.
Create src/tasks/dto/task-response.dto.ts
:
import { ApiProperty } from '@nestjs/swagger';
export class TaskResponseDto {
@ApiProperty({
description: 'Unique task ID',
example: '123'
})
id: string;
@ApiProperty({
description: 'Task title',
example: 'Buy groceries'
})
title: string;
@ApiProperty({
description: 'Task description',
example: 'Milk, eggs, bread',
required: false
})
description?: string;
@ApiProperty({
description: 'When the task was created',
example: '2024-01-15T10:30:00Z'
})
createdAt: Date;
}
Update your controller:
import { TaskResponseDto } from './dto/task-response.dto';
@Post()
@ApiOperation({ summary: 'Create a new task' })
@ApiResponse({
status: 201,
description: 'Task created successfully',
type: TaskResponseDto // Changed this!
})
create(@Body() createTaskDto: CreateTaskDto): TaskResponseDto {
return {
id: '123',
...createTaskDto,
createdAt: new Date()
};
}
Refresh Swagger. Now the response section shows the complete structure including id
and createdAt
. Your teammates know exactly what they're getting back.
When things go wrong (and how to fix them)
My Swagger UI is empty, no endpoints show up
This usually means your DTOs don't have @ApiProperty()
decorators. Swagger silently ignores classes without them. Go add those decorators to every field you want documented.
The "Authorize" button isn't there
Check two things. First, did you add .addBearerAuth()
in main.ts
? Second, did you add @ApiBearerAuth()
to your controller? You need both.
Swagger shows "object" instead of my DTO structure
You probably forgot to add type: YourDto
in your @ApiResponse()
decorator. If it's an array, use type: [YourDto]
with brackets.
My endpoints are all mixed together
Add @ApiTags('group-name')
at the top of each controller. This groups related endpoints together in the UI.
Changes aren't showing up
Your dev server might not have reloaded. Stop it (Ctrl+C) and run npm run start:dev
again. Sometimes decorators need a fresh start.
What to do next
You have working Swagger docs! Here's how to make them even better:
Document query parameters using @ApiQuery()
. If your GET endpoint accepts filters like ?status=done
, document them so people know they exist.
Document path parameters like /tasks/:id
using @ApiParam()
. Swagger will show the :id
part as a field users need to fill.
Add multiple examples to show different use cases. Maybe one example for a basic task, another for a task with all optional fields filled.
Version your docs if you're building v2 of your API. You can create separate Swagger pages at /docs/v1
and /docs/v2
just like the payments example from your codebase. Each version includes only its own modules.
Here's how versioning looks:
// v2 docs
const v2Config = new DocumentBuilder()
.setTitle('Tasks API v2')
.setVersion('2.0.0')
.addBearerAuth()
.build();
const v2Document = SwaggerModule.createDocument(app, v2Config, {
include: [TasksV2Module] // Only v2 endpoints
});
SwaggerModule.setup('docs/v2', app, v2Document);
Remember these things
Swagger works best when you think of it as part of your code, not separate documentation. Every time you add a field to a DTO, add @ApiProperty()
. Every time you create an endpoint, add @ApiOperation()
. It takes 10 seconds and saves your teammates hours.
The example
field in @ApiProperty()
is your friend. Real examples help people understand faster than descriptions.
If someone has to ask you "what does this endpoint do?" after looking at your Swagger docs, something's missing. That's your cue to improve the docs.
Good documentation means fewer interruptions, which means more time to build features. Your future self will thank you when you come back to this code in six months.
Your mental model for Swagger
Think of it like this. Your main.ts
file is where you turn on the lights (the /docs
route). Your DTOs are the vocabulary (what words mean what). Your controllers are the instruction manual (what each button does). The decorators are the highlighter pen that makes important stuff visible.
Without decorators, Swagger is blind. With them, it shows everything clearly.
If you only remember one thing from this guide, remember to decorate your DTOs. Everything else follows from that.
One last thing
Got questions? The most common issue is forgetting @ApiProperty()
on a DTO field. ALways ensure You Check that first.
Happy Coding : - )