Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке pipes у NestJS?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Pipes у NestJS** - це функції, які трансформують і валідують вхідні дані запиту до того, як вони потраплять до обробника маршруту, реалізуючи інтерфейс `PipeTransform`. ```typescript @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.usersService.findOne(id); // id - число або 400 } ``` **Ключове:** Express завжди передає параметри URL як рядки; pipes конвертують їх у потрібний тип або кидають 400 до виконання обробника.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Pipes у NestJS** - це функції, що перехоплюють вхідні дані запиту (параметри, query, body) до того, як вони потрапляють до обробника маршруту, реалізуючи інтерфейс `PipeTransform`. Вони або перетворюють сире значення на потрібний тип, або кидають HTTP 400, якщо дані невалідні. ## Теорія ### TL;DR - Уяви pipe як сканер в аеропорту: сирі рядки з URL перевіряються і конвертуються; невалідні дані відхиляються одразу, правильні проходять у потрібному типі - Два завдання: трансформація (`"123"` → `123`) і валідація (відхилити не-ціле число, невалідний email, неправильний UUID) - HTTP завжди передає параметри і query-рядки як strings. Pipes виправляють це автоматично - `ValidationPipe` для валідації DTO; `ParseIntPipe`, `ParseUUIDPipe` - для примітивів - Правило: завжди використовуй pipes для вхідних HTTP-даних; для довірених внутрішніх викликів можна пропустити ### Швидкий приклад ```typescript // Без pipe: id завжди рядок з URL @Get(':id') findOne(@Param('id') id: string) { return this.usersService.findOne(+id); // ручний парсинг, падає на "abc" } // З ParseIntPipe: автоматичний парсинг і валідація, кидає 400 при помилці @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.usersService.findOne(id); // id - число або 400 Bad Request } // GET /users/123 → id = 123 // GET /users/abc → 400 "Validation failed (numeric string is expected)" ``` `ParseIntPipe` виконується до тіла обробника. Якщо значення не є цілим числом, NestJS кидає `BadRequestException` і обробник не викликається взагалі. ### Головна відмінність від ручного парсингу Ручний парсинг всередині обробника (`+id`, `parseInt(id)`) змішує валідацію даних з бізнес-логікою. На невалідному вводі він тихо повертає `NaN`, який потім потрапляє в сервіс і викликає збої далеко від джерела. Pipes перехоплюють проблему на вході, до будь-якої логіки, і повертають стандартну відповідь 400. ### Коли використовувати - **URL-параметри і query-рядки**: завжди використовуй `ParseIntPipe`, `ParseUUIDPipe` тощо. Express передає їх як рядки. - **Тіло POST/PUT-запиту**: `ValidationPipe` разом із DTO-класом і декораторами class-validator. - **Необов'язкові query-параметри зі значеннями за замовчуванням**: постав `DefaultValuePipe` перед `ParseIntPipe`. - **Глобальна валідація**: `app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }))` в `main.ts`. - **Перевірки на рівні бізнес-логіки** (унікальність email, існування в БД): кастомний pipe з реалізацією `PipeTransform`. ### Вбудовані pipes | Pipe | Що робить | Приклад | |---|---|---| | `ValidationPipe` | Валідує DTO через class-validator | `{ age: "25" }` → `{ age: 25 }` або 400 | | `ParseIntPipe` | рядок → ціле число, відхиляє не-цілі | `"123"` → 123; `"abc"` → 400 | | `ParseFloatPipe` | рядок → число з плаваючою комою | `"12.5"` → 12.5 | | `ParseBoolPipe` | рядок → булевий тип | `"true"` → true | | `ParseUUIDPipe` | Валідує формат UUID | невалідний UUID → 400 | | `ParseEnumPipe` | Валідує проти значень enum | значення не в enum → 400 | | `DefaultValuePipe` | Повертає значення за замовчуванням | немає `limit` → 10 | | `ParseArrayPipe` | Рядок через кому → масив | `"1,2,3"` → [1, 2, 3] | ### Рівні прив'язки Pipes можна застосовувати на чотирьох рівнях. Більш специфічний завжди перекриває менш специфічний. ```typescript // 1. Рівень параметра (найбільш специфічний) @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) {} // 2. Рівень методу @Post() @UsePipes(ValidationPipe) create(@Body() dto: CreateUserDto) {} // 3. Рівень контролера @Controller('users') @UsePipes(new ValidationPipe()) export class UsersController {} // 4. Глобальний (в main.ts) app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); // або через DI-токен: { provide: APP_PIPE, useClass: ValidationPipe } ``` Глобальні pipes виконуються на кожному запиті у всьому додатку. Це правильний підхід для `ValidationPipe`. Для парсингу конкретних типів, наприклад `ParseIntPipe`, застосовуй на рівні параметра - там, де тобі справді потрібне число. ### Як NestJS виконує pipes Під час реєстрації маршрутів NestJS прив'язує pipes до конкретних аргументів, використовуючи метадані з декораторів (`@Param`, `@Body`, `@Query`). На кожному запиті він послідовно викликає `transform(value, metadata)` для кожного pipe перед тим, як передати аргументи до обробника. Якщо `transform()` кидає `BadRequestException`, фільтр виключень повертає 400. Обробник не викликається. Кастомні pipes можуть бути `async`. NestJS чекає на результат перед тим, як продовжити, тому запити до БД всередині `transform()` працюють без жодного додаткового налаштування. ### Типові помилки **Помилка 1:** `new ValidationPipe()` без `{ transform: true }`. Без цього опції поля DTO залишаються рядками навіть якщо типізовані як `number`. Валідатори на кшталт `@IsNumber()` тоді падають на рядковому значенні. З того, що я бачив у продакшн-проектах, це найпоширеніша помилка з pipes: все виглядає правильно, поки десь тихо не зламається числове порівняння. ```typescript // Неправильно: age залишається "25" (рядок), @IsNumber() не спрацьовує app.useGlobalPipes(new ValidationPipe()); // Правильно app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); ``` **Помилка 2:** `@Body(ValidationPipe)` без DTO-класу. `ValidationPipe` очікує клас із метаданими від декораторів. Без DTO немає чого валідувати і кидається незрозуміла помилка. ```typescript // Неправильно @Post() create(@Body(ValidationPipe) body: any) {} // Правильно: завжди разом з DTO @Post() create(@Body() dto: CreateUserDto) {} ``` **Помилка 3:** Неправильний порядок pipes на одному параметрі. Pipes виконуються зліва направо. Якщо поставити `ValidationPipe` перед `ParseIntPipe`, він намагатиметься валідувати рядок, а не число. ```typescript // Неправильно: валідує рядок, потім парсить @Param('id', ValidationPipe, ParseIntPipe) // Правильно: спочатку парсимо, потім валідуємо @Param('id', ParseIntPipe, ValidationPipe) ``` **Помилка 4:** `new Error()` в кастомному pipe замість `BadRequestException`. Звичайна `Error` стає відповіддю 500 Internal Server Error. Pipes знаходяться в HTTP-шарі і мають кидати HTTP-виключення NestJS. ```typescript // Неправильно: відповідь 500 throw new Error('Invalid value'); // Правильно: відповідь 400 throw new BadRequestException('Invalid value'); ``` **Помилка 5:** Забутий `async/await` у кастомному pipe з запитом до БД. ```typescript // Неправильно: повертає нерозв'язаний Promise як значення transform(value: string) { const exists = this.service.exists(value); // немає await! if (exists) throw new BadRequestException(); return value; } // Правильно async transform(value: string) { const exists = await this.service.exists(value); if (exists) throw new BadRequestException('Вже зайнятий'); return value; } ``` ### Де зустрічається в реальних проектах - `main.ts` більшості NestJS-додатків: глобальний `ValidationPipe` з `whitelist: true` і `transform: true` - Ендпоінти авторизації: `ParseUUIDPipe` на `@Param('userId')` відхиляє невалідні ID до звернення до БД - Prisma-додатки: кастомні pipes валідують значення enum перед передачею в Prisma-запити - GraphQL-резолвери: `ValidationPipe` на `@Args()` працює так само, як і на REST-контролерах - Мікросервіси NestJS: pipes валідують payload для `MessagePattern`, той самий API `PipeTransform` ### Питання на співбесіді **Q:** Яка різниця між глобальним pipe і pipe на рівні маршруту? **A:** Глобальні pipes виконуються на кожному запиті в усіх контролерах. Pipes на рівні маршруту або параметра - тільки там, де задекоровано. Глобальний підхід для загальної валідації DTO, локальний для конкретного перетворення типів. **Q:** Чи може pipe отримати доступ до повного об'єкта HTTP-запиту? **A:** Не напряму через аргументи `transform()`. В кастомному pipe можна заінжектити `REQUEST` scope або використати `ExecutionContext`. Більшість задач цього не потребує. **Q:** Як `ValidationPipe` обробляє вкладені DTO? **A:** Додай `@ValidateNested()` на вкладену властивість і `@Type(() => NestedDto)` з class-transformer. Для масивів використовуй `each: true` у валідаторі. **Q:** Що відбувається, якщо перший з двох pipes на параметрі кидає виключення? **A:** Виконання зупиняється одразу. Другий pipe не виконується, обробник не викликається, виключення передається до фільтра виключень. **Q (Senior):** Тобі потрібно обмежити кількість запитів за IP через Redis всередині pipe. Які підводні камені? **A:** Потрібен request scope для читання IP - через `Inject(REQUEST)` або `ExecutionContext`. `transform()` має бути `async`. Огорни виклик Redis у try/catch з fallback, інакше таймаут Redis перетворюється на 500. Тестуй під конкурентним навантаженням: без атомарних операцій (`INCR` + `EXPIRE` в одній транзакції) виникнуть race conditions. ## Приклади ### Базовий: парсинг параметра URL ```typescript import { Controller, Get, Param } from '@nestjs/common'; import { ParseIntPipe } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get(':id') findCat(@Param('id', ParseIntPipe) id: number) { // id завжди число тут return `Cat #${id}`; } } // GET /cats/5 → "Cat #5" // GET /cats/foo → 400 "Validation failed (numeric string is expected)" ``` `ParseIntPipe` виконується до тіла методу. Обробник отримує тільки валідні цілі числа. ### Середній: валідація DTO на POST-ендпоінті ```typescript import { IsEmail, IsInt, Min } from 'class-validator'; import { Body, Controller, Param, Post } from '@nestjs/common'; import { ParseIntPipe, ValidationPipe } from '@nestjs/common'; class UpdateUserDto { @IsEmail() email: string; @IsInt() @Min(18) age: number; } @Controller('users') export class UsersController { @Post(':id') updateUser( @Param('id', ParseIntPipe) id: number, @Body(new ValidationPipe({ transform: true })) dto: UpdateUserDto, ) { // id - число, email валідований, age - число >= 18 return this.usersService.update(id, dto); } } // POST /users/1 { "email": "test@example.com", "age": "20" } // → dto = { email: "test@example.com", age: 20 } // POST /users/1 { "email": "bad", "age": 15 } // → 400 з деталями помилок валідації ``` Опція `transform: true` конвертує `"20"` (рядок з JSON) в `20` (число) до того, як спрацює перевірка `@IsInt()`. Без неї перевірка падає на рядку. ### Просунутий: кастомний асинхронний pipe для перевірки унікальності ```typescript import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; import { UsersService } from './users.service'; @Injectable() export class UniqueEmailPipe implements PipeTransform<string> { constructor(private readonly usersService: UsersService) {} async transform(value: string, metadata: ArgumentMetadata): Promise<string> { const exists = await this.usersService.emailExists(value); if (exists) { throw new BadRequestException(`Email "${value}" вже зареєстрований`); } return value; } } // Використання в контролері @Post() async createUser( @Body('email', UniqueEmailPipe) email: string, ) { return this.usersService.create({ email }); } ``` Три речі роблять це правильним. `UniqueEmailPipe` позначений `@Injectable()`, тому NestJS сам обробляє залежності і сервіс доступний через конструктор. `transform()` - `async`, і NestJS чекає на результат перед викликом обробника. І `BadRequestException` замість звичайної `Error` - відповідь буде 400, а не 500.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.