Validacija podataka u NestJS-u korišćenjem “Pipe”

Uvodu u Nest.js “Pipe”

Pipes u NestJS-u su specijalne komponente koje se koriste za transformaciju i validaciju podataka pre nego što stignu do kontrolera ili metoda kontrolera. Pipes su korisni kada želimo da osiguramo da podaci koje primamo u API zahtevima ispunjavaju određene uslove ili da se na specifičan način transformišu.

Nest.js dolazi sa ugrađenim pipes koji se mogu koristiti za transformaciju i validaciju podataka. Ugrađeni pipe-ovi, pružaju brza i efikasna rešenja za osnovne zadatke kao što su validacija i transformacija. Evo nekih ugrađeni pipes:

  • ValidationPipe: Automatska validacija podataka koristeći dekoratore.
  • ParseIntPipe: Automatska konverzija stringa u broj.
  • ParseBoolPipe: Automatska konverzija stringa u boolean.
  • ParseArrayPipe: Automatska konverzija stringa u niz.
  • ParseUUIDPipe: Automatska validacija UUID formata.
  • DefaultValuePipe: Postavljanje podrazumevane vrednosti ako vrednost nije definisana.

Međutim, ako su vaši zahtevi specifični, možete kreirati “custom pipe-ove” koristeći interfejs “PipeTransform”.

pipes

ValidationPipe

Ako se “ValidationPipe” koriste za DTO validaciju onda je potrebno da instaliramo dodatne biblioteke (kod drugih ugrađenih pipe-ove to nije potrebno). Ovo su dve neophodne biblioteke:

  • class-validator: Omogućava definisanje i proveru pravila validacije.
  • class-transformer: Transformiše ulazne podatke u instance klasa DTO-a.

Koristite sledeću komandu za instalaciju:

npm install class-validator class-transformer
  

Šta je “class-transformer”?

“class-transformer” je TypeScript biblioteka koja omogućava pretvaranje (transformaciju) plain JavaScript objekata u instance klasa i obrnuto. Obično su to jednostavni JavaScript objekti (dobijeni, na primer, iz JSON-a) u instance definisanih klasa. Takođe omogućava dodavanje logike za prilagođene transformacije i rad sa ugneždenim strukturama objekata.

Primer: JS objekt u instancu klase

import { plainToClass } from 'class-transformer';

class User {
  firstName: string;
  lastName: string;
}

const plainObject = { firstName: 'John', lastName: 'Doe' };
const user = plainToClass(User, plainObject);

console.log(user instanceof User); // true
  

Primer: klasa u plain JS objekat

import { classToPlain } from 'class-transformer';

const user = new User();
user.firstName = 'John';
user.lastName = 'Doe';

const plainObject = classToPlain(user);
console.log(plainObject); // { firstName: 'John', lastName: 'Doe' }
  

Šta je “class-validator”?

“class-validator” je TypeScript biblioteka koja omogućava definisanje i proveru pravila validacije za JavaScript objekte. Ova biblioteka koristi dekoratore za definisanje pravila validacije, kao što su provera tipova, dužine, minimalne i maksimalne vrednosti, obavezna polja, regularni izrazi i mnoga druga pravila.

Ovo su najčešće korišćeni:

  • @IsString(): Proverava da li je vrednost string.
  • @IsInt(): Proverava da li je vrednost integer.
  • @IsBoolean(): Proverava da li je vrednost boolean.
  • @IsEmail(): Proverava da li je vrednost validna email adresa.
  • @IsNotEmpty(): Proverava da li je vrednost prazna.
  • @MinLength(): Proverava minimalnu dužinu stringa.
  • @MaxLength(): Proverava maksimalnu dužinu stringa.
  • @Min(): Proverava minimalnu vrednost broja.
  • @Max(): Proverava maksimalnu vrednost broja.
  • @Matches(): Proverava da li vrednost odgovara regularnom izrazu.

Listu svih dekoratora možete pronaći na ovome linku.

Primer
import { IsString } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;
}
  

ValidationPipe opcije

Validation Pipe u NestJS-u ima nekoliko korisnih opcija za prilagođavanje ponašanja prilikom validacije. Ove opcije omogućavaju detaljniju kontrolu nad načinom na koji će validacija biti izvršena i pomažu pri rukovanju sa nepotrebnim ili nevalidnim podacima. U nastavku su objašnjene glavne opcije koje možete koristiti sa Validation Pipe-om.

a) Opcija whitelist

Opcija whitelist služi za automatsko uklanjanje svih svojstava koja nisu definisana u DTO (Data Transfer Object) klasi. Kada je ova opcija omogućena, svi dodatni podaci koji nisu eksplicitno definisani u DTO klasi biće ignorisani, čime se osigurava da samo željena svojstva budu obrađena.

Primer:

@UsePipes(new ValidationPipe({ whitelist: true }))
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
    return createUserDto;
}
  
b) Opcija forbidNonWhitelisted

Opcija forbidNonWhitelisted radi u kombinaciji sa whitelist. Kada je omogućena, ova opcija baca grešku svaki put kada se pojavi neko polje koje nije definisano u DTO klasi, umesto da ga samo ignoriše. Ovo je korisno ako želite striktno kontrolisati podatke i sprečiti prolazak bilo kakvih dodatnih informacija koje ne pripadaju zahtevanoj strukturi.

Primer:

@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
    return createUserDto;
}
  
c) Opcija transform

Opcija transform omogućava automatsko prebacivanje ulaznih podataka u instance DTO klasa. Kada je omogućena, ulazni podaci će se automatski transformisati u instancu DTO klase, što omogućava lakši pristup metodama i tipovima u okviru objekta.

Primer:

@UsePipes(new ValidationPipe({ transform: true }))
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
    return createUserDto;
}
  

ValidatePipe na nivou parametra

`ValidationPipe` se može primeniti na pojedinačni parametar rute za validaciju ulaznih podataka.

import { Controller, Get, Param, ValidationPipe } from '@nestjs/common';
import { IsUUID } from 'class-validator';

class FindUserDto {
  @IsUUID()
  id: string;
}

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id', new ValidationPipe()) id: string) {
    return `User with ID: ${id}`;
  }
}

Ako korisnik pošalje zahtev sa nevalidnim id (npr. nije UUID), API vraća grešku:

{
  "statusCode": 400,
  "message": ["id must be a UUID"],
  "error": "Bad Request"
}

ValidatePipe na nivou metode

Dekorator @UsePipes se koristi da primeni ValidationPipe na nivou metode kontrolera.

import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { IsString, IsInt, Min } from 'class-validator';

class CreateUserDto {
  @IsString()
  name: string;

  @IsInt()
  @Min(1)
  age: number;
}

@Controller('users')
export class UsersController {
  @Post()
  @UsePipes(ValidationPipe)
  createUser(@Body() createUserDto: CreateUserDto) {
    return `User created with name: ${createUserDto.name}, age: ${createUserDto.age}`;
  }
}

Validan zahtev:

{
  "name": "John",
  "age": 25
}

Nevalidan zahtev:

{
  "name": "",
  "age": -1
}

API odgovara sa greškom:

{
  "statusCode": 400,
  "message": [
    "name should not be empty",
    "age must not be less than 1"
  ],
  "error": "Bad Request"
}

ValidatePipe na nivou klase

Dekorator @UsePipes može se primeniti na klasu kontrolera, čime se ValidationPipe primenjuje na sve metode u okviru te klase.

import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { IsString, IsInt, Min } from 'class-validator';

class CreateUserDto {
  @IsString()
  name: string;

  @IsInt()
  @Min(1)
  age: number;
}

@UsePipes(ValidationPipe)
@Controller('users')
export class UsersController {
  @Post()
  createUser(@Body() createUserDto: CreateUserDto) {
    return `User created with name: ${createUserDto.name}, age: ${createUserDto.age}`;
  }

  @Post('update')
  updateUser(@Body() updateUserDto: CreateUserDto) {
    return `User updated with name: ${updateUserDto.name}, age: ${updateUserDto.age}`;
  }
}

Prednost: Svi zahtevi u okviru kontrolera automatski prolaze validaciju, što smanjuje potrebu za pojedinačnim deklarisanjem.

ValidatePipe na globalnom nivou

ValidationPipe se može registrovati kao globalni Pipe, čime se primenjuje na sve rute u aplikaciji.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Globalna registracija ValidationPipe-a
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }));

  await app.listen(3000);
}
bootstrap();

Opcije za ValidationPipe:

  • whitelist: true – Automatski uklanja svojstva koja nisu definisana u DTO klasi.
  • forbidNonWhitelisted: true – Baca grešku ako se pojave nepoznata svojstva.
  • transform: true – Transformiše ulazne podatke u instance DTO klasa.

Prednost: Validacija se primenjuje automatski na sve zahteve, bez potrebe za deklarisanjem u svakom kontroleru.

Custom pipes

Ako su vaši zahtevi specifični, onda je nepohodno kreirati “custom pipe-ove”. Za kreiranje pipes, je potrebno:

  1. Kreirate novu klasu koja implementira PipeTransform interfejs.
  2. Implementirate metodu transform(), koja prima dva parametra:
    • value: Ulazna vrednost koja dolazi iz zahteva.
    • metadata: Informacije o vrednosti (opciono, sadrži podatke o tipu i dekoratorima).

Primer 1: Pipe za validaciju broja

Kreiramo Pipe koji proverava da li je ulazni string validan broj. Ako nije, baca grešku.

import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform {
  transform(value: any): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed: Not a number');
    }
    return val;
  }
}
Korišćenje u kontroleru:
import { Controller, Get, Param } from '@nestjs/common';
import { ParseIntPipe } from './parse-int.pipe';

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return `User with ID ${id}`;
  }
}

Ako klijent pošalje zahtev na /users/123, vrednost 123 se uspešno konvertuje u broj.

Ako pošalje /users/abc, API baca grešku:

{
  "statusCode": 400,
  "message": "Validation failed: Not a number",
  "error": "Bad Request"
}

Primer 2: Pipe za dozvoljene vrednosti

Pipe proverava da li je vrednost iz liste dozvoljenih vrednosti.

import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class AllowedValuesPipe implements PipeTransform {
  constructor(private readonly allowedValues: any[]) {}

  transform(value: any): any {
    if (!this.allowedValues.includes(value)) {
      throw new BadRequestException(`Value '${value}' is not allowed.`);
    }
    return value;
  }
}
Korišćenje u kontroleru:
import { Controller, Get, Query } from '@nestjs/common';
import { AllowedValuesPipe } from './allowed-values.pipe';

@Controller('products')
export class ProductsController {
  @Get()
  findByType(@Query('type', new AllowedValuesPipe(['electronics', 'furniture'])) type: string) {
    return `Products of type: ${type}`;
  }
}

Validan zahtev: /products?type=electronics

Nevalidan zahtev: /products?type=clothing

{
  "statusCode": 400,
  "message": "Value 'clothing' is not allowed.",
  "error": "Bad Request"
}

Primer 3: Pipe za transformaciju u veliki tekst

Pipe transformiše string u uppercase.

import { PipeTransform, Injectable } from '@nestjs/common';

@Injectable()
export class UppercasePipe implements PipeTransform {
  transform(value: string): string {
    return value.toUpperCase();
  }
}
    
Korišćenje u kontroleru:
import { Controller, Post, Body } from '@nestjs/common';
import { UppercasePipe } from './uppercase.pipe';

@Controller('messages')
export class MessagesController {
  @Post()
  createMessage(@Body('text', UppercasePipe) text: string) {
    return `Message: ${text}`;
  }
}
    

Ulaz: {“text”: “hello world”}

Izlaz: Message: HELLO WORLD