Як документувати API NestJS за допомогою Swagger?
@nestjs/swagger генерує інтерактивну OpenAPI документацію з TypeScript декораторів на контролерах, DTO та маршрутах. Жодного ручного YAML-файлу, жодної окремої документації, яку потрібно тримати в синхроні.
Теорія
TL;DR
- Встанови
@nestjs/swagger, викличSwaggerModule.setup()уmain.ts, відкрий/api/docs - Аналогія: інструкція IKEA, яка збирається сама. Декоратори в коді - це список деталей, Swagger UI - готовий план із кнопкою "Спробувати"
@ApiProperty()на полях DTO,@ApiTags()і@ApiResponse()на контролерах- Документація оновлюється при кожному рестарті - завжди відповідає актуальним маршрутам
- Не потрібно для внутрішніх мікросервісів без зовнішніх споживачів
Швидке налаштування
// main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Users API')
.setDescription('Управління користувачами')
.setVersion('1.0')
.addBearerAuth() // додає кнопку "Authorize" в UI
.addTag('users')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document); // UI доступний за /api/docs
await app.listen(3000);
}
bootstrap();
// Відкрий http://localhost:3000/api/docsDocumentBuilder збирає конфіг OpenAPI. createDocument() сканує всі завантажені модулі та будує специфікацію. setup() роздає її як HTML-сторінку. Три виклики - готово.
Декорування DTO та контролерів
Кожне поле DTO потребує @ApiProperty(). Без нього Swagger показує поле як {} без типу та прикладу.
// create-user.dto.ts
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'alice@example.com', description: 'Унікальний email' })
email: string;
@ApiProperty({ example: 'Alice', minLength: 2 })
name: string;
@ApiPropertyOptional({ example: 25, minimum: 18 })
age?: number;
@ApiProperty({ enum: ['user', 'admin'], default: 'user' })
role: string;
}Контролери отримують @ApiTags() для групування, @ApiOperation() для опису та @ApiResponse() для кожного статус-коду.
// users.controller.ts
import { Controller, Post, Get, Body, Param } from '@nestjs/common';
import {
ApiTags, ApiOperation, ApiResponse,
ApiBearerAuth, ApiParam, ApiQuery
} from '@nestjs/swagger';
@ApiTags('users') // групує маршрути під вкладкою "users"
@ApiBearerAuth() // іконка замка; потрібен JWT
@Controller('users')
export class UsersController {
@Get()
@ApiOperation({ summary: 'Отримати всіх користувачів' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiResponse({ status: 200, description: 'Список користувачів', type: [UserResponseDto] })
findAll() { /* ... */ }
@Get(':id')
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 200, type: UserResponseDto })
@ApiResponse({ status: 404, description: 'Не знайдено' })
findOne(@Param('id') id: string) { /* ... */ }
@Post()
@ApiOperation({ summary: 'Створити нового користувача' })
@ApiResponse({ status: 201, type: UserResponseDto })
@ApiResponse({ status: 400, description: 'Помилка валідації' })
create(@Body() dto: CreateUserDto) { /* ... */ }
}Головна відмінність від ручної документації
Postman-колекції та README застарівають. Маршрут змінився - хтось забув оновити документацію - і вона вже бреше. Swagger сканує декоратори при запуску і щоразу будує свіжий JSON. Код - єдине джерело правди. Жодного дублювання.
Один патерн, який добре працює на практиці: експортувати /api/docs-json у CI і порівнювати з попередньою версією як автоматичну перевірку на breaking changes.
Коли використовувати
- Фронтенд або мобільна команда споживає API: Swagger дає кнопку "Try it" без читання коду
- QA тестує auth-потоки:
@ApiBearerAuth()і кнопка "Authorize" дозволяють вставити JWT прямо в UI - Монорепозиторій із кількома сервісами: спільний
/docsендпоінт прискорює інтеграцію між командами - Версіонований API:
.setVersion()і.addTag()документують зміни - Прототип до 5 ендпоінтів без зовнішніх споживачів: налаштовувати не варто
Як це працює при запуску
SwaggerModule.createDocument() використовує reflect-metadata для сканування кожного @Controller(), @Get(), @Post() і @ApiProperty() декоратора в усіх модулях, завантажених у DI-контейнер NestJS. В пам'яті будується OpenAPI v3 JSON-об'єкт. Потім setup() роздає два ресурси за вказаним шляхом: сирий JSON за [path]-json (наприклад, /api/docs-json) і HTML-сторінку Swagger UI, яка читає цей JSON. UI-ресурси беруться з @nestjs/swagger/dist/ui. Нічого не компілюється заздалегідь - все відбувається при кожному старті.
Типові помилки
Відсутній імпорт reflect-metadata
// Неправильно: відсутній на початку main.ts
// @ApiProperty() метадані зникають без жодної помилкиNestJS CLI додає це автоматично. Кастомні налаштування іноді пропускають. Додай import 'reflect-metadata' першим рядком у main.ts, якщо документи порожні.
Циклічні посилання між DTO
// Неправильно: UserDto посилається на ProfileDto, а ProfileDto - на UserDto
// Error: Maximum call stack size exceeded
// Виправлення: фабрична функція розриває цикл
@ApiProperty({ type: () => ProfileDto })
profile: ProfileDto;@ApiProperty() на private полях
// Неправильно
@ApiProperty()
private email: string; // Swagger показує поле, TypeScript приховує його під час виконанняВикористовуй public поля. Або @Expose() із class-transformer, якщо потрібен контроль серіалізації.
Відсутній @ApiTags() на контролерах
Без тегів усі ендпоінти опиняються в групі "default". При 30+ маршрутах це стає непрацездатним. Додай @ApiTags('users') до кожного контролера і за потреби .addTag('users', 'Управління користувачами') в DocumentBuilder для описів.
Завантаження файлів без @ApiConsumes()
@Post('upload')
@ApiConsumes('multipart/form-data') // обов'язково, інакше Swagger показує JSON-поле
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) { /* ... */ }Без цього декоратора UI відображає текстове поле JSON замість файлового завантажувача.
Несумісність версій
@nestjs/swagger@7.1+ потрібен для NestJS 10. Старіші версії ламають генерацію enum та деякі схеми відповідей без очевидних повідомлень про помилку. Перевір версії обох пакетів перед тим, як шукати баг.
Де використовується
- NestJS CRUD-бойлерплейти (Trilon стартери): Swagger з першого дня, нові розробники не читають контролери під час онбордингу
- Auth-мікросервіс:
@ApiBearerAuth()документує JWT-потік, QA тестує захищені маршрути через UI - Міграція моноліту в мікросервіси: експорт
/api/docs-jsonдля contract testing із Pact - Захист у продакшені: загорни
SwaggerModule.setup()уif (process.env.NODE_ENV !== 'production')або проксіюй/api/docsчерез auth guard
Питання на співбесіді
Q: Як документувати enum та вкладені DTO?
A: Для enum: @ApiProperty({ enum: Category, enumName: 'Category' }) - відображається як випадаючий список в UI. Для вкладених об'єктів: @ApiProperty({ type: () => AddressDto }). Для масивів: @ApiProperty({ type: [ImageDto] }). Форма з фабричною функцією type: () => X також запобігає помилкам циклічних посилань.
Q: Чи читає Swagger декоратори class-validator автоматично?
A: Ні. @IsEmail() і @IsString() не впливають на схему Swagger. Потрібен @ApiProperty() на кожному полі. Деякі команди використовують хелпери @nestjs/mapped-types - PartialType() і OmitType() - які автоматично переносять метадані @ApiProperty() з базового класу.
Q: Як згенерувати клієнтський SDK з документації?
A: Отримай специфікацію з /api/docs-json і передай її в openapi-generator-cli або Orval. Orval популярний у NestJS монорепозиторіях для генерації типізованих React Query хуків прямо зі специфікації.
Q: (Senior) Як документувати маршрути із динамічними guards?
A: Guards не впливають на схему. Додай @ApiBearerAuth() до контролера і @ApiResponse({ status: 401, description: 'Unauthorized' }) до кожного захищеного методу. Для динамічних сегментів шляху @ApiParam({ name: 'id', type: Number, example: 42 }) гарантує робоче поле введення в UI.
Q: Як захистити /api/docs у продакшені?
A: Два підходи. Перший: if (process.env.NODE_ENV !== 'production') SwaggerModule.setup(...) - документи існують лише в dev та staging. Другий: проксіюй маршрут документів через guard, який перевіряє сесію або внутрішній API-ключ. Другий підхід підходить для staging, де QA потрібен доступ.
Приклади
Базовий: DTO з enum та необов'язковим полем
// create-product.dto.ts
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsEnum, IsOptional } from 'class-validator';
enum Category {
Electronics = 'electronics',
Clothing = 'clothing',
}
export class CreateProductDto {
@ApiProperty({ example: 'Laptop Pro', minLength: 2 })
@IsString()
name: string;
@ApiProperty({ enum: Category, enumName: 'Category' }) // випадаючий список в UI
@IsEnum(Category)
category: Category;
@ApiPropertyOptional({ type: [String], example: ['sale', 'new'] })
@IsOptional()
tags?: string[];
}
// Swagger UI: текстове поле name, випадаючий список category, необов'язковий масив tagsEnum перетворюється на випадаючий список в UI. ApiPropertyOptional позначає поле як необов'язкове в схемі. Обидва декоратори відповідають тому, що class-validator перевіряє під час виконання.
Середній: Auth-ендпоінт з Bearer-токеном
// auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { ApiProperty } from '@nestjs/swagger';
export class LoginDto {
@ApiProperty({ example: 'alice@example.com' })
email: string;
@ApiProperty({ example: 'secret123' })
password: string;
}
@Controller('auth')
@ApiTags('auth')
export class AuthController {
@Post('login')
@ApiOperation({ summary: 'Увійти та отримати JWT-токен' })
@ApiResponse({ status: 200, description: 'Повертає access_token' })
@ApiResponse({ status: 401, description: 'Невірні дані' })
login(@Body() loginDto: LoginDto) {
return { access_token: 'jwt-token' };
}
}
// У DocumentBuilder в main.ts додай:
// .addBearerAuth()
// На захищеному контролері:
// @ApiBearerAuth()
// Це пов'язує маршрут з кнопкою "Authorize" в UI.addBearerAuth() у main.ts реєструє схему безпеки (security scheme). @ApiBearerAuth() на контролері підключає його до цієї схеми. QA-інженери натискають "Authorize" в UI, вставляють токен і тестують захищені маршрути без жодної curl-команди.
Розширений: Response DTO з виключенням полів
// user-response.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
export class UserResponseDto {
@ApiProperty()
id: number;
@ApiProperty()
name: string;
@ApiProperty()
email: string;
@Exclude() // виключається зі схеми Swagger і з HTTP-відповіді
password: string;
}
// users.controller.ts
import {
UseInterceptors, ClassSerializerInterceptor,
SerializeOptions, Get, Param
} from '@nestjs/common';
import { ParseIntPipe } from '@nestjs/common';
import { ApiResponse } from '@nestjs/swagger';
@Get(':id')
@UseInterceptors(ClassSerializerInterceptor)
@SerializeOptions({ type: UserResponseDto })
@ApiResponse({ status: 200, type: UserResponseDto })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id); // поле password видаляється перед відправкою
}@Exclude() із class-transformer прибирає поле password зі схеми Swagger і з реальної HTTP-відповіді. Використовуй ClassSerializerInterceptor глобально через APP_INTERCEPTOR в модулі або для конкретного маршруту через @UseInterceptors().
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.