Пошук уроків, статей та іншого контенту
Зрозумієте призначення pipes, їхній життєвий цикл і способи підключення до параметрів, методів та контролерів.
Pipe — це клас, який обробляє вхідні дані перед передаванням їх у метод контролера.
Pipes використовують для двох основних завдань:
трансформації даних, наприклад перетворення рядка "42" на число 42;
валідації даних і відхилення некоректних запитів.
Pipe отримує значення параметра, а також метадані про нього, і повертає:
перетворене значення;
те саме значення без змін;
помилку, якщо значення не проходить перевірку.
Якщо pipe викидає виняток, метод контролера не виконується.
HTTP-запит
↓
guards
↓
pipes
↓
метод контролера
↓
сервісУ повному життєвому циклі NestJS pipes виконуються після middleware і guards, але до виклику методу контролера.
NestJS має кілька готових pipes для типових перетворень і перевірок:
ParseIntPipe — перетворює значення на ціле число;
ParseFloatPipe — перетворює значення на число з плаваючою крапкою;
ParseBoolPipe — перетворює значення на boolean;
ParseUUIDPipe — перевіряє UUID;
ParseArrayPipe — перевіряє та перетворює масиви;
DefaultValuePipe — задає значення за замовчуванням;
ValidationPipe — перевіряє DTO за допомогою декораторів валідації.
Параметри HTTP-запиту зазвичай надходять як рядки. Наприклад, значення id у маршруті /users/42 спочатку має тип string.
ParseIntPipe перетворить його на number:
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return {
id,
type: typeof id,
};
}
}Для запиту:
GET /users/42метод отримає:
{
"id": 42,
"type": "number"
}Якщо передати некоректне значення:
GET /users/abcNestJS автоматично поверне помилку 400 Bad Request, а метод findOne не буде викликаний.
Pipe можна застосувати до кожного параметра окремо:
import {
Controller,
Get,
Param,
ParseIntPipe,
ParseUUIDPipe,
} from '@nestjs/common';
@Controller('orders')
export class OrdersController {
@Get(':orderId/items/:itemId')
findItem(
@Param('orderId', ParseUUIDPipe) orderId: string,
@Param('itemId', ParseIntPipe) itemId: number,
) {
return {
orderId,
itemId,
};
}
}У цьому прикладі:
orderId перевіряється як UUID;
itemId перетворюється на ціле число.
Для кожного значення, до якого підключено pipe, NestJS виконує приблизно такі кроки:
Отримує значення з HTTP-запиту.
Визначає тип параметра та його джерело:
@Param();
@Query();
@Body();
@Headers();
@UploadedFile().
Викликає метод transform.
Передає результат у метод контролера.
Якщо transform викидає виняток, формує HTTP-помилку.
Pipe працює не з усім HTTP-запитом, а з конкретним значенням, до якого його підключили. Тому pipe, встановлений на @Param('id'), отримує саме значення id.
Метод transform також отримує об’єкт метаданих:
import {
ArgumentMetadata,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class LogValuePipe implements PipeTransform {
transform(value: unknown, metadata: ArgumentMetadata) {
console.log({
value,
type: metadata.type,
data: metadata.data,
metatype: metadata.metatype,
});
return value;
}
}Основні властивості ArgumentMetadata:
type — джерело значення: body, query, param або custom;
data — ім’я параметра, наприклад id;
metatype — тип параметра, відомий під час виконання, наприклад String або Number.
Власний pipe — це клас, який реалізує інтерфейс PipeTransform.
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParsePositiveIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata): number {
const parsedValue = Number(value);
if (!Number.isInteger(parsedValue) || parsedValue <= 0) {
throw new BadRequestException(
`Параметр "${metadata.data}" має бути додатним цілим числом`,
);
}
return parsedValue;
}
}Цей pipe:
отримує рядкове значення;
перетворює його на число;
перевіряє, що число є цілим і додатним;
повертає число;
викидає BadRequestException, якщо значення некоректне.
import { Controller, Get, Param } from '@nestjs/common';
import { ParsePositiveIntPipe } from './parse-positive-int.pipe';
@Controller('products')
export class ProductsController {
@Get(':id')
findOne(@Param('id', ParsePositiveIntPipe) id: number) {
return {
id,
message: 'Товар знайдено',
};
}
}Тепер:
GET /products/15передасть у метод число 15.
А запит:
GET /products/0завершиться помилкою 400 Bad Request.
@InjectableПростий pipe можна створити без декоратора @Injectable, якщо йому не потрібні залежності з DI-контейнера:
import {
ArgumentMetadata,
BadRequestException,
PipeTransform,
} from '@nestjs/common';
export class TrimPipe implements PipeTransform {
transform(value: unknown, metadata: ArgumentMetadata) {
if (typeof value !== 'string') {
throw new BadRequestException(
`Параметр "${metadata.data}" має бути рядком`,
);
}
return value.trim();
}
}Для pipe із залежностями сервісів краще використовувати @Injectable() і передавати клас, щоб NestJS міг створити його через dependency injection.
Pipe можна підключити на трьох основних рівнях:
до окремого параметра;
до методу контролера;
до всього контролера.
Також pipe може бути глобальним для всього застосунку.
Це найточніший спосіб підключення. Pipe працює лише з одним параметром одного методу.
import { Controller, Get, Param } from '@nestjs/common';
import { ParsePositiveIntPipe } from './parse-positive-int.pipe';
@Controller('products')
export class ProductsController {
@Get(':id')
findOne(@Param('id', ParsePositiveIntPipe) id: number) {
return { id };
}
}Такий підхід доречний, коли правило стосується лише конкретного параметра.
За допомогою @UsePipes() можна застосувати pipe до всіх аргументів конкретного методу:
import {
Body,
Controller,
Post,
UsePipes,
} from '@nestjs/common';
import { TrimPipe } from './trim.pipe';
@Controller('messages')
export class MessagesController {
@Post()
@UsePipes(TrimPipe)
create(@Body() body: { text: string }) {
return {
text: body.text,
};
}
}У цьому випадку TrimPipe буде викликаний для параметрів методу, до яких NestJS застосовує pipes. Для об’єктів і складних структур потрібно чітко визначити, яке саме значення pipe має трансформувати.
Частіше власні pipes підключають безпосередньо до конкретного параметра:
@Post()
create(@Body('text', TrimPipe) text: string) {
return { text };
}Якщо pipe має діяти для всіх маршрутів контролера, його можна підключити над класом:
import {
Controller,
Get,
Param,
UsePipes,
} from '@nestjs/common';
import { ParsePositiveIntPipe } from './parse-positive-int.pipe';
@Controller('products')
@UsePipes(ParsePositiveIntPipe)
export class ProductsController {
@Get(':id')
findOne(@Param('id') id: number) {
return { id };
}
@Get(':id/reviews')
findReviews(@Param('id') id: number) {
return {
productId: id,
reviews: [],
};
}
}Pipe, підключений на рівні контролера, застосовується до параметрів методів цього контролера.
Однак для pipes, які очікують конкретний формат значення, важливо перевірити, до яких параметрів вони застосовуються. Наприклад, pipe для цілих чисел не повинен обробляти об’єкт @Body() або текстовий query-параметр.
Глобальний pipe працює для всіх контролерів застосунку. Його можна підключити в main.ts:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);
await app.listen(3000);
}
bootstrap();Глобальні pipes зручні для загальних правил, наприклад:
валідації DTO у всіх маршрутах;
автоматичної трансформації вхідних даних;
видалення невідомих полів із тіла запиту.
Не варто підключати глобально pipe, який призначений лише для одного конкретного параметра.
У NestJS pipe може бути встановлений на різних рівнях. Загальна ідея така:
глобальні pipes мають найширшу область;
controller-level pipes діють у межах контролера;
method-level pipes діють у межах одного методу;
parameter-level pipes діють для конкретного аргументу.
Параметрний pipe виконується перед викликом методу контролера. Якщо на одному параметрі встановлено кілька pipes, вони утворюють послідовність обробки: результат одного pipe передається наступному.
import { Controller, Get, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(
@Param(
'id',
(value: string) => value.trim(),
(value: string) => Number(value),
)
id: number,
) {
return { id };
}
}Для складної логіки краще створити окремий клас pipe. Це спрощує тестування та повторне використання.
ValidationPipe і DTOValidationPipe використовується для перевірки об’єктів запиту за допомогою DTO.
DTO описує очікувану структуру даних:
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(3)
name: string;
}Підключення ValidationPipe до конкретного маршруту:
import {
Body,
Controller,
Post,
ValidationPipe,
} from '@nestjs/common';
import { CreateUserDto } from './create-user.dto';
@Controller('users')
export class UsersController {
@Post()
create(
@Body(new ValidationPipe({ whitelist: true }))
dto: CreateUserDto,
) {
return dto;
}
}Якщо тіло запиту не відповідає DTO, NestJS поверне помилку 400 Bad Request.
Параметр whitelist: true видаляє поля, для яких у DTO немає validation-декораторів. Щоб не просто видаляти невідомі поля, а відхиляти запит із ними, можна використати forbidNonWhitelisted: true:
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
})За замовчуванням дані HTTP-запиту надходять у JavaScript як рядки або звичайні об’єкти. Опція transform: true дозволяє ValidationPipe перетворювати значення відповідно до типів DTO та параметрів, коли це підтримується конфігурацією:
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);Не слід покладатися лише на TypeScript-тип:
@Get(':id')
findOne(@Param('id') id: number) {
// Без pipe або transform значення може фактично залишатися рядком.
return { id };
}TypeScript-тип number не виконує перетворення під час роботи програми. Для явного перетворення параметра використовуйте ParseIntPipe або інший відповідний pipe.
Нижче наведено мінімальний приклад контролера з різними способами використання pipes:
import {
BadRequestException,
Body,
Controller,
Get,
Param,
ParseIntPipe,
Post,
Query,
ValidationPipe,
} from '@nestjs/common';
import { IsEmail, IsString, MinLength } from 'class-validator';
class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(2)
name: string;
}
class ParseSortPipe {
transform(value: unknown): 'asc' | 'desc' {
if (value === undefined) {
return 'asc';
}
if (value !== 'asc' && value !== 'desc') {
throw new BadRequestException(
'Параметр sort має бути "asc" або "desc"',
);
}
return value;
}
}
@Controller('users')
export class UsersController {
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
@Query('sort', new ParseSortPipe()) sort: 'asc' | 'desc',
) {
return {
id,
sort,
};
}
@Post()
create(
@Body(new ValidationPipe({ whitelist: true }))
dto: CreateUserDto,
) {
return {
message: 'Користувача створено',
user: dto,
};
}
}Приклади запитів:
GET /users/10?sort=descРезультат:
{
"id": 10,
"sort": "desc"
}POST /users
Content-Type: application/json
{
"email": "user@example.com",
"name": "Олена",
"unknownField": true
}За умови whitelist: true поле unknownField буде видалене перед передаванням DTO в метод контролера.
@Get(':id')
findOne(@Param('id') id: number) {
// Фактичне значення може бути рядком.
return id;
}Анотація : number перевіряється під час компіляції, але не перетворює HTTP-значення під час виконання.
Правильно:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return id;
}Метод transform має повернути значення, яке буде передане далі:
transform(value: string) {
value.trim();
}У такому випадку результатом буде undefined.
Правильно:
transform(value: string) {
return value.trim();
}parseInt без повної перевіркиconst result = parseInt('123abc', 10);Результатом буде 123, хоча все значення не є коректним числом.
Для параметрів маршрутів краще використовувати ParseIntPipe, який перевіряє вхідне значення, або створити власний pipe з потрібними правилами.
throw new Error('Некоректне значення');Звичайна помилка не описує коректно HTTP-відповідь. Для помилки валідації використовуйте HTTP-виняток NestJS:
throw new BadRequestException('Некоректне значення');Pipe для додатного числа не варто встановлювати на весь контролер, якщо в ньому є параметри-рядки, об’єкти або масиви.
Підключайте pipe на найвужчому необхідному рівні:
@Get(':id')
findOne(@Param('id', ParsePositiveIntPipe) id: number) {
return { id };
}undefinedQuery-параметр може бути відсутнім:
GET /usersPipe, який очікує лише рядок, має або обробити undefined, або явно визначити, що параметр є обов’язковим.
Pipe обробляє дані перед викликом методу контролера.
Основні завдання pipes — трансформація та валідація.
Вхідні значення HTTP-запиту часто є рядками, тому для перетворення потрібно використовувати pipes.
Вбудовані pipes, такі як ParseIntPipe і ValidationPipe, покривають поширені сценарії.
Власний pipe реалізує PipeTransform і містить метод transform.
Pipe може бути підключений до параметра, методу, контролера або всього застосунку.
Якщо pipe викидає виняток, NestJS припиняє обробку запиту й повертає помилку.
TypeScript-анотація типу сама по собі не змінює дані під час виконання.