Skip to main content

Як документувати 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() на контролерах
  • Документація оновлюється при кожному рестарті - завжди відповідає актуальним маршрутам
  • Не потрібно для внутрішніх мікросервісів без зовнішніх споживачів

Швидке налаштування

typescript
// 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/docs

DocumentBuilder збирає конфіг OpenAPI. createDocument() сканує всі завантажені модулі та будує специфікацію. setup() роздає її як HTML-сторінку. Три виклики - готово.

Декорування DTO та контролерів

Кожне поле DTO потребує @ApiProperty(). Без нього Swagger показує поле як {} без типу та прикладу.

typescript
// 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() для кожного статус-коду.

typescript
// 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

typescript
// Неправильно: відсутній на початку main.ts // @ApiProperty() метадані зникають без жодної помилки

NestJS CLI додає це автоматично. Кастомні налаштування іноді пропускають. Додай import 'reflect-metadata' першим рядком у main.ts, якщо документи порожні.

Циклічні посилання між DTO

typescript
// Неправильно: UserDto посилається на ProfileDto, а ProfileDto - на UserDto // Error: Maximum call stack size exceeded // Виправлення: фабрична функція розриває цикл @ApiProperty({ type: () => ProfileDto }) profile: ProfileDto;

@ApiProperty() на private полях

typescript
// Неправильно @ApiProperty() private email: string; // Swagger показує поле, TypeScript приховує його під час виконання

Використовуй public поля. Або @Expose() із class-transformer, якщо потрібен контроль серіалізації.

Відсутній @ApiTags() на контролерах

Без тегів усі ендпоінти опиняються в групі "default". При 30+ маршрутах це стає непрацездатним. Додай @ApiTags('users') до кожного контролера і за потреби .addTag('users', 'Управління користувачами') в DocumentBuilder для описів.

Завантаження файлів без @ApiConsumes()

typescript
@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 та необов'язковим полем

typescript
// 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, необов'язковий масив tags

Enum перетворюється на випадаючий список в UI. ApiPropertyOptional позначає поле як необов'язкове в схемі. Обидва декоратори відповідають тому, що class-validator перевіряє під час виконання.

Середній: Auth-ендпоінт з Bearer-токеном

typescript
// 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 з виключенням полів

typescript
// 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().

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?