Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як документувати API NestJS за допомогою Swagger?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`@nestjs/swagger`** генерує OpenAPI документацію з TypeScript декораторів при запуску застосунку. Встанови пакет, налаштуй `SwaggerModule.setup()` у `main.ts` і відкрий `/api/docs` для інтерактивного UI. ```typescript const config = new DocumentBuilder() .setTitle('Users API').setVersion('1.0').addBearerAuth().build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api/docs', app, document); ``` Додай `@ApiProperty()` до полів DTO та `@ApiTags()`, `@ApiOperation()`, `@ApiResponse()` до контролерів. **Ключове:** документація оновлюється при кожному рестарті і завжди відповідає актуальним маршрутам.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`@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()`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.