IlmHamroh
JavaScript Full-stack/30-qism. NestJS15/65-dars23 daqiqa
Mundarija (34)

DTO va class-validator NestJS'da: dekoratorli qoidalar, ValidationError va o'z validatorlar

Qisqacha: DTO — mijozdan keladigan ma'lumotning shakli, oddiy klass. Uning maydonlari ustiga class-validator dekoratorlari qo'yiladi: @IsString(), @Length(1, 100), @IsEmail(), @IsEnum(Rol), @IsOptional(). Tekshiruv ikki qadamda: class-transformer JSON obyektni klass nusxasiga aylantiradi (plainToInstance), keyin validate() qoidalarni bajaradi va ValidationError ro'yxatini qaytaradi. Nest'da bu ishni ValidationPipe qiladi. Eng ko'p uchraydigan tuzoq: bir maydondagi dekoratorlar pastdan yuqoriga ishlaydi.

Bu darsda

  • DTO va entity (ombordagi to'liq yozuv) farqini tushuntira olasiz va nega DTO klass bo'lishi kerakligini ko'rasiz.
  • class-validator'ning asosiy dekoratorlari bilan DTO yozasiz va ValidationError daraxtini o'qiysiz.
  • O'z validatoringizni ikki usulda yozasiz: @ValidatorConstraint + @Validate va registerDecorator.
  • @Transform va @Type bilan qiymatni tekshiruvdan oldin tayyorlaysiz.
  • vazifalar-api ning class-validator variantini ko'rasiz va nega u hozircha loyihaga kirmasligini bilasiz.

Oldin bilishingiz kerak: Backend validatorlari: Joi, class-validator va express-validator, Legacy dekoratorlar va reflect-metadata, Request lifecycle, Reflector, metadata va DiscoveryService.

O'tgan darsda Nest dekoratorlar qoldirgan metama'lumotni qanday o'qishini ko'rdik. Bu dars va keyingi beshta darsda shu bilim eng ko'p ishlatiladigan joyga — kirish tekshiruviga qo'llanadi.

1. Nega bu kerak?

Controller'lar darsida bitta ogohlantirish bor edi: @Body() body: YangiVazifa — tekshiruv emas. TypeScript turi ish vaqtida yo'q. Hujumchi {"matn": 5, "admin": true} yuborsa, u shu "tur" bilan metodga kiradi.

vazifalar-api da hozircha bu muammoni 25-qismdagi usul hal qiladi: handler boshida tekshir(YangiVazifaSxemasi, req.body, "body"). Ishlaydi, lekin har handler'da qo'lda yoziladi va @Req() ga bog'lanadi.

Nest'da kirishni tekshiradigan maxsus qatlam bor — pipe. Request lifecycle darsidagi tartibni eslang: middleware → guard → interceptor → pipe → handler. Pipe har parametrni handler'ga berishdan oldin ko'radi. Savol: pipe nimaga qarab tekshiradi?

Javob — parametr turiga. Lekin tur faqat klass bo'lsa ish vaqtida qoladi. Shuning uchun Nest dunyosida kirish shakli — klass, qoidalari — shu klass maydonlaridagi dekoratorlar. Bunday klass DTO deyiladi.

O'xshatish: bankdagi anketa blanki. Har katakcha yonida qoida yozilgan: "faqat raqam", "16 ta xona", "ixtiyoriy". Kassir (pipe) blankni qabul qilishdan oldin har katakni shu qoida bo'yicha tekshiradi. Blankni to'ldirgan odam qoidani o'zgartira olmaydi.

Nest loyihasiga ishga kirsangiz, birinchi ko'radigan narsalaringizdan biri — create-user.dto.ts kabi fayllar va ichidagi @IsEmail(). Shuning uchun bu dars standartni to'liq o'rgatadi, garchi bizning loyiha boshqa yo'lni tanlagan bo'lsa ham (sababi — o'lchov bilan, Zod va Standard Schema NestJS'da darsida).

2. DTO nima

2.1 DTO va entity

DTO (Data Transfer Object — ma'lumot uzatish obyekti) — bir qatlamdan boshqasiga o'tadigan ma'lumot shakli. Bu atamani Next.js'dagi DAL darsida ham ko'rgansiz. Backend'da u ko'pincha "mijoz nima yuborishi mumkin" degan ma'noda ishlatiladi.

Entity — ombordagi to'liq yozuv. vazifalar-api da bu Vazifa:

Maydon Vazifa (entity) YangiVazifaDto (POST) OzgarishDto (PATCH)
id, versiya bor yo'q yo'q
matn bor majburiy ixtiyoriy
bajarildi bor ixtiyoriy ixtiyoriy
teglar, yaratildi bor yo'q (server hisoblaydi) yo'q

Nega entity'ni to'g'ridan-to'g'ri @Body() turi qilmaymiz? Chunki unda id, versiya bor — mijoz ularni yubormasligi kerak. Agar yuborgan qiymat omborga yetib borsa, bu ommaviy tayinlash (mass assignment) zaifligi (Validatsiya: nima uchun, qayerda va qanday). Har amal uchun alohida DTO — "shu amalda mijozdan faqat shu maydonlar".

2.2 Nega klass, interfeys emas

Bu farqni brauzerda ko'rib chiqamiz. Blok TypeScript'ni dekoratorlar bilan kompilyatsiya qiladi va reflect-metadata ishlatadi — Nest ham aynan shunday ishlaydi:

ts
// @ts-options: {"experimentalDecorators": true, "emitDecoratorMetadata": true}
import "reflect-metadata";

interface YangiVazifa { matn: string }

const Belgi = (): PropertyDecorator => () => {};

class YangiVazifaDto {
  @Belgi() matn!: string;
  @Belgi() bajarildi?: boolean;
}

const tur = (k: object, m: string): unknown =>
  Reflect.getMetadata("design:type", k, m);
console.log(tur(YangiVazifaDto.prototype, "matn"));
console.log(tur(YangiVazifaDto.prototype, "bajarildi"));
console.log(typeof YangiVazifaDto);
const bosh: YangiVazifa = { matn: "Non olish" };
console.log(Object.keys(bosh));

Konsolda:

text
[Function: String]
[Function: Boolean]
function
[ 'matn' ]

Klass ish vaqtida bor (function), va dekorator qo'yilgan har maydon uchun TypeScript design:type metama'lumotini yozib qo'ydi: String, Boolean. YangiVazifa interfeysi esa kompilyatsiyadan keyin butunlay yo'qoladi — undan ish vaqtida so'raydigan hech narsa qolmaydi. Pipe ham, class-validator ham klassdan o'qiydi. Qoida shundan kelib chiqadi: DTO — klass.

Tekshirib ko'ring: Hamkasbingiz export interface CreateUserDto { email: string } yozdi va @Body() dto: CreateUserDto qildi. Global ValidationPipe yoqilgan. Noto'g'ri email bilan so'rov o'tadimi?

Javob

O'tadi. Interfeys ish vaqtida yo'q — Nest parametr turi sifatida Object ni ko'radi, unda hech qanday qoida yo'q. ValidationPipe tekshiradigan narsa topmaydi va qiymatni o'zgarishsiz uzatadi. DTO klass bo'lishi va maydonlarida class-validator dekoratorlari turishi shart.

3. class-validator: takror va chuqurroq

3.1 Ikki qadam

Backend validatorlari darsida class-validator'ni Zod bilan taqqoslagan edik. Asosiy g'oyani eslaymiz: tekshiruv ikki qadamda bo'ladi.

flowchart LR
  A["JSON tana<br/>{ matn: '' }"] --> B["plainToInstance<br/>(class-transformer)"]
  B --> C["YangiVazifaDto<br/>nusxasi"]
  C --> D["validate()<br/>(class-validator)"]
  D --> E{"xato bormi?"}
  E -- "yo'q" --> F["[] — handler'ga"]
  E -- "bor" --> G["ValidationError[]"]

Birinchi qadam oddiy obyektni klass nusxasiga aylantiradi. Ikkinchi qadam o'sha nusxaning klassidan dekoratorlar qoldirgan qoidalarni topadi va bajaradi. Diagrammadagi asosiy narsa — o'rtadagi nusxa: qoidalar klassga bog'langan, oddiy obyektda ular yo'q.

Tajriba uchun alohida kichik loyiha qildik: Nest CLI bilan yaratilgan demo, ustiga class-validator 0.15.1 va class-transformer 0.5.1 (biz ishga tushirdik: Node 24.21.0, NestJS 12.2.0, TypeScript 6.0.3). Birinchi DTO:

ts
import { plainToInstance } from 'class-transformer';
import {
  IsInt, IsOptional, IsString, Length, Max, Min, validate,
} from 'class-validator';
import type { ValidationError } from 'class-validator';

class YangiVazifaDto {
  @IsString()
  @Length(1, 100)
  matn!: string;

  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(5)
  muhimlik?: number;
}

// o'qish uchun: har xatodan uch maydon, bir qatorda
const qisqa = (x: ValidationError[]) => x.map((e) => ({
  maydon: e.property, qiymat: e.value, qoidalar: e.constraints,
}));

const yaroqli = plainToInstance(YangiVazifaDto, {
  matn: 'Non olish',
});
console.log(
  yaroqli instanceof YangiVazifaDto,
  JSON.stringify(await validate(yaroqli)),
);
const yomon = plainToInstance(YangiVazifaDto, {
  matn: '', muhimlik: 9,
});
for (const e of qisqa(await validate(yomon))) {
  console.log(JSON.stringify(e));
}

Chiqish:

text
true []
{"maydon":"matn","qiymat":"","qoidalar":{"isLength":"matn must be longer than or equal to 1 characters"}}
{"maydon":"muhimlik","qiymat":9,"qoidalar":{"max":"muhimlik must not be greater than 5"}}

Yaroqli nusxada xato yo'q — bo'sh massiv. Yaroqsizda ikkita xato: matn bo'sh, muhimlik 5 dan katta.

3.2 ValidationError ni o'qish

Har xato — ValidationError obyekti. Uning to'rt maydoni sizga kerak bo'ladi:

Maydon Nima Misol
property qaysi maydon "matn"
value kelgan qiymat ""
constraints buzilgan qoidalar: { qoidaNomi: xabar } { isLength: "..." }
children ichma-ich obyekt xatolari keyingi darslarda

constraints — obyekt: bitta maydon bir nechta qoidani buzishi mumkin, har biri o'z kaliti bilan. isLength, max — qoida nomlari, ular xabarni almashtirsangiz ham o'zgarmaydi. Mijozga javob yasashda shu kalitlarga tayanish mumkin.

children ichma-ich DTO'larda to'ladi va daraxt hosil qiladi. Uni tekis ro'yxatga aylantirish — «Xato daraxtini tekislash» mashqi.

3.3 Oddiy obyekt tuzog'i

Birinchi qadamni tashlab ketsak nima bo'ladi?

ts
console.log(JSON.stringify(qisqa(await validate({ matn: 5 }))));
text
[{"qoidalar":{"unknownValue":"an unknown value was passed to the validate function"}}]

Tarjimasi: "validate funksiyasiga noma'lum qiymat berildi". { matn: 5 } — hech qaysi klassning nusxasi emas. class-validator unda qoidalarni topa olmadi va bitta umumiy xato berdi. E'tibor bering: xato matn haqida emas — maydon nomi ham, qoida ham yo'q.

Bu xulq forbidUnknownValues sozlamasidan keladi; biz o'rnatgan 0.15.1 versiyada u sukut bo'yicha yoqilgan. Backend validatorlari darsida bu tuzoqni "tekshiruv jim o'tadi" deb aytgan edik. Biz 0.15.1 da qayta tekshirdik: jim o'tmaydi, lekin baribir maydon qoidalari ishlamaydi. Xulosa o'zgarmaydi — plainToInstance siz validate foydasiz. Nest'da bu ikki qadamni ValidationPipe o'zi bajaradi, shuning uchun bu xatoga ko'pincha Nest'dan tashqarida (skript, worker, test) duch kelasiz.

3.4 Ko'p ishlatiladigan dekoratorlar

Dekorator Tekshiradi
@IsString(), @IsBoolean(), @IsInt(), @IsNumber() JavaScript turi
@Length(min, max), @MinLength, @MaxLength satr uzunligi
@Min(n), @Max(n), @IsPositive() son oralig'i
@IsEmail(), @IsUrl(), @IsUUID() format
@IsEnum(Rol), @IsIn([...]) ruxsat etilgan qiymatlar
@Matches(/regex/) naqsh
@IsArray(), @ArrayMaxSize(n) massiv
@IsOptional() qiymat yo'q bo'lsa — qolgan qoidalarni o'tkazib yuborish

Har dekoratorga oxirgi argument sifatida sozlama beriladi: { message: "..." } — o'z xabaringiz, { each: true } — massivning har elementiga, { groups: [...] } — guruhlar (keyingi darslarda).

Foydalanuvchi ro'yxatdan o'tadigan DTO bilan sinadik:

ts
enum Rol { Foydalanuvchi = 'foydalanuvchi', Admin = 'admin' }

class RoyxatdanOtishDto {
  @IsEmail({}, { message: "email noto'g'ri" })
  email!: string;

  @IsEnum(Rol)
  rol!: Rol;

  @IsOptional()
  @Length(2, 30)
  ism?: string;
}

{ email: "aziz@", rol: "boss" } uchun:

text
{"maydon":"email","qiymat":"aziz@","qoidalar":{"isEmail":"email noto'g'ri"}}
{"maydon":"rol","qiymat":"boss","qoidalar":{"isEnum":"rol must be one of the following values: foydalanuvchi, admin"}}

email — o'zimiz bergan o'zbekcha xabar. rol — sukut inglizcha xabar: "rol quyidagi qiymatlardan biri bo'lishi kerak: foydalanuvchi, admin". ism umuman yo'q — @IsOptional() tufayli @Length tekshirilmadi.

@IsEmail ning birinchi argumenti — {} — email formati sozlamalari (masalan, domen talab qilinadimi). Ikkinchisi — xabar. Bu ikki argumentli shakl faqat format dekoratorlarida bor; adashtirsangiz, xabar sozlama o'rniga ketib qoladi va inglizcha xabar chiqaveradi.

Diqqat: @IsOptional() qiymat undefined bo'lganda ham, null bo'lganda ham qolgan qoidalarni o'tkazib yuboradi. {"ism": null} — o'tadi. PATCH so'rovida bu jiddiy xato beradi — Mapped types va nested validatsiya darsida haqiqiy chiqish bilan ko'ramiz.

4. Dekoratorlar tartibi tuzog'i

4.1 Model: qoidalar qaysi tartibda yoziladi

Legacy dekoratorlar darsidagi qoidani eslang: bitta maydon ustidagi dekoratorlar pastdan yuqoriga qo'llanadi. class-validator har dekoratorni qo'llanganda qoidani ro'yxatga qo'shadi. Natijada ro'yxat tartibi — kodda ko'rganingizning teskarisi.

Buni class-validator'siz, kichik modelda ko'raylik. Model ham Nest kabi reflect-metadata da saqlaydi:

ts
// @ts-options: {"experimentalDecorators": true}
import "reflect-metadata";

type Qoida = { nomi: string; tekshir: (v: unknown) => boolean };
const QOIDALAR = Symbol("qoidalar");

function qoidaQosh(nomi: string, tekshir: Qoida["tekshir"]) {
  return (obyekt: object, maydon: string): void => {
    console.log(`qo'shildi: ${maydon} ← ${nomi}`);
    const eski: Record<string, Qoida[]> =
      Reflect.getMetadata(QOIDALAR, obyekt) ?? {};
    eski[maydon] = [...(eski[maydon] ?? []), { nomi, tekshir }];
    Reflect.defineMetadata(QOIDALAR, eski, obyekt);
  };
}
const SatrBolsin = () =>
  qoidaQosh("satr", (v) => typeof v === "string");
const Uzunlik = (min: number, max: number) =>
  qoidaQosh(`uzunlik ${min}–${max}`, (v) =>
    typeof v === "string" && v.length >= min && v.length <= max);

class YangiVazifaDto {
  @SatrBolsin()
  @Uzunlik(1, 100)
  matn!: string;
}

function tekshir(
  klass: { prototype: object },
  qiymat: Record<string, unknown>,
) {
  const qoidalar: Record<string, Qoida[]> =
    Reflect.getMetadata(QOIDALAR, klass.prototype) ?? {};
  const xatolar: string[] = [];
  for (const [maydon, royxat] of Object.entries(qoidalar)) {
    for (const q of royxat) {
      if (!q.tekshir(qiymat[maydon])) {
        xatolar.push(`${maydon}: ${q.nomi}`);
      }
    }
  }
  return xatolar;
}

console.log(tekshir(YangiVazifaDto, { matn: "Non olish" }));
console.log(tekshir(YangiVazifaDto, { matn: 5 }));

Konsolda:

text
qo'shildi: matn ← uzunlik 1–100
qo'shildi: matn ← satr
[]
[ 'matn: uzunlik 1–100', 'matn: satr' ]

Kodda @SatrBolsin() tepada, lekin ro'yxatga birinchi bo'lib uzunlik tushdi. Shuning uchun { matn: 5 } uchun birinchi xato — "uzunlik", garchi asl muammo "satr emas" bo'lsa ham.

Model soddalashtirilgan: class-validator qoidalarni o'z ichki omborida saqlaydi (biz reflect-metadata ni ko'rsatish uchun ishlatdik) va har qoida uchun alohida xabar yasaydi. Tartib qoidasi esa aynan shunday.

4.2 Haqiqiy class-validator

Bitta xato yetarli bo'lsa, stopAtFirstError: true beriladi — har maydon uchun birinchi buzilgan qoida qaytadi. Ikki klass bilan sinadik — farqi faqat dekoratorlar tartibida:

ts
class TartibA { @IsString() @Length(1, 100) matn!: string; }
class TartibB { @Length(1, 100) @IsString() matn!: string; }

for (const K of [TartibA, TartibB]) {
  const x = await validate(plainToInstance(K, {}), {
    stopAtFirstError: true,
  });
  console.log(K.name, JSON.stringify(x[0]?.constraints));
}
text
TartibA {"isLength":"matn must be longer than or equal to 1 characters"}
TartibB {"isString":"matn must be a string"}

{} — matn umuman yo'q. To'g'ri xabar — "satr bo'lishi kerak". TartibA da (@IsString tepada) xabar "uzunlik kamida 1 belgi" chiqdi — mijoz "bo'sh satr yubordim shekilli" deb adashadi. TartibB da (@IsString pastda) xabar to'g'ri.

Qoida: tur tekshiruvi (@IsString, @IsInt) — maydondagi dekoratorlarning eng pastida. Kanon tajribasida ham aynan shu xato bo'lgan: class-validator variantini yozayotganda {} uchun "1–100 belgi" chiqqan va testlar ushlagan. Variantning har DTO'sida shuning uchun izoh bor ("Loyiha qadami" bo'limiga qarang).

Tekshirib ko'ring: @IsInt() @Min(1) limit!: number maydoniga "abc" keldi, stopAtFirstError: true. Qaysi xabar chiqadi va nega?

Javob

min xabari: "limit must not be less than 1". Dekoratorlar pastdan yuqoriga qo'llanadi — @Min birinchi tekshiriladi va "abc" uni buzadi. To'g'risi — @Min(1) @IsInt() limit (tur tekshiruvi pastda), shunda "must be an integer number" chiqadi.

5. O'z validatoringiz

Tayyor dekoratorlar yetmaydigan joy tez keladi: "teg # bilan boshlansin", "sana haqiqiy kalendar kuni bo'lsin". class-validator'da buning ikki yo'li bor.

5.1 @ValidatorConstraint + @Validate

Qoida — ValidatorConstraintInterface ni bajaradigan klass: validate() — true/false, defaultMessage() — xabar.

ts
@ValidatorConstraint({ name: 'tegFormati' })
class TegFormati implements ValidatorConstraintInterface {
  validate(qiymat: unknown): boolean {
    return typeof qiymat === 'string'
      && /^#[\p{L}\p{N}_]{1,30}$/u.test(qiymat);
  }
  defaultMessage(a: ValidationArguments): string {
    return `${a.property}: "#" bilan boshlangan so'z bo'lsin`;
  }
}

class TegDto {
  @Validate(TegFormati)
  birinchi!: string;
}

ValidationArguments — xabar yasash uchun kontekst: property (maydon nomi), value, object (butun DTO), constraints.

5.2 registerDecorator — o'z dekoratoringiz

@Validate(TegFormati) o'qishga noqulay. Uni @IsTeg() ga aylantiramiz:

ts
function IsTeg(sozlama?: ValidationOptions): PropertyDecorator {
  return (obyekt, maydon) => {
    registerDecorator({
      name: 'isTeg',
      target: obyekt.constructor,
      propertyName: String(maydon),
      options: sozlama,
      validator: TegFormati,
    });
  };
}

class TegDto {
  @Validate(TegFormati)
  birinchi!: string;

  @IsTeg({ each: true })
  boshqalar!: string[];
}

{ birinchi: "uy", boshqalar: ["#ish", "bozor"] } uchun:

text
{"maydon":"birinchi","qiymat":"uy","qoidalar":{"tegFormati":"birinchi: \"#\" bilan boshlangan so'z bo'lsin"}}
{"maydon":"boshqalar","qiymat":["#ish","bozor"],"qoidalar":{"tegFormati":"boshqalar: \"#\" bilan boshlangan so'z bo'lsin"}}

Ikki narsaga qarang. { each: true } — qoida massivning har elementiga: "#ish" o'tdi, "bozor" o'tmadi va butun maydon xato bo'ldi. constraints kaliti — tegFormati, ya'ni validator klassi nomi (@ValidatorConstraint({ name })), dekorator nomi emas.

validate() Promise<boolean> ham qaytarishi mumkin — masalan, "bu email bazada bormi". Bunda @ValidatorConstraint({ async: true }) qo'yiladi. Lekin bazaga murojaat qiladigan tekshiruvni DTO'ga qo'yishdan ehtiyot bo'ling. Har so'rov uchun qo'shimcha so'rov bazaga boradi. Baribir ombor qatlami ham ushlashi kerak: ikki so'rov bir vaqtda "yo'q" deb ko'rishi mumkin (poyga holati).

5.3 Nomsiz validator tuzog'i

Kanon tajribasida yana bir haqiqiy xato topilgan. limit va offset uchun "butun son, oraliqda" qoidasini bitta yordamchi funksiya yasagan, ichida nomsiz klass bilan:

ts
// XATO: har chaqiruvda yangi klass, lekin nomi bir xil — "Oraliq"
const butunOraliq = (nom: string, min: number, max: number) => {
  class Oraliq implements ValidatorConstraintInterface {
    validate(v: unknown) {
      return Number(v) >= min && Number(v) <= max;
    }
  }
  return Validate(Oraliq);
};

Natija: limit=0 o'tib ketdi (200). Sabab: class-validator constraint klasslarini nomi bo'yicha saqlaydi. Ikkala chaqiruv Oraliq nomli klass yasadi; ikkinchisi birinchisining o'rnini egalladi va limit uchun offset ning oralig'i (0..10 000) ishladi. Tuzatish — har klassga noyob nom:

ts
ValidatorConstraint({ name: `${nom}Oraliq` })(Oraliq);

Bu xato testlarsiz sezilmasdi: hech qanday ogohlantirish yo'q, faqat chegaradagi qiymat noto'g'ri o'tadi. 25-qismdagi limit=0 testlari shuni ushladi.

6. class-transformer: tekshiruvdan oldin tayyorlash

6.1 @Transform va @Type

Zod'da .trim() sxemaning bir qismi edi. class-validator faqat tekshiradi, o'zgartirmaydi. Qiymatni o'zgartirish — class-transformer'ning ishi, va u birinchi qadamda (plainToInstance) bajariladi:

ts
class KesilganDto {
  @Transform(({ value }) =>
    typeof value === 'string' ? value.trim() : value)
  @IsString()
  @Length(1, 100)
  matn!: string;

  @Type(() => Number)
  @IsInt()
  limit!: number;
}
  • @Transform(fn) — maydon qiymatini fn qaytargani bilan almashtiradi.
  • @Type(() => Number) — qiymatni berilgan turga aylantiradi (Number("20")).

Ikki kirish bilan sinadik — {"matn":" Non ","limit":"20"} va {"matn":" ","limit":"abc"}. Har biri uchun: nusxa, limit turi (birinchisida) va xatolar:

text
{"matn":"Non","limit":20} number []
{"matn":"","limit":null} [{"maydon":"matn","qiymat":"","qoidalar":{"isLength":"matn must be longer than or equal to 1 characters"}},{"maydon":"limit","qiymat":null,"qoidalar":{"isInt":"limit must be an integer number"}}]

Birinchisi: bo'sh joylar kesildi, "20" son bo'ldi, xato yo'q. Ikkinchisi qiziqroq: " " kesilib "" bo'ldi va keyin @Length uni ushladi — kesish tekshiruvdan oldin bo'lgani uchun to'g'ri. "abc" esa Number("abc") = NaN bo'ldi (JSON'da NaN null bo'lib ko'rinadi). @IsInt uni rad etdi — yaxshi. Lekin @IsInt bo'lmaganida, NaN handler'gacha yetib borardi.

6.2 Nima uchun @Transform ichida tur tekshiruvi

typeof value === 'string' ? value.trim() : value — nega shunchaki value.trim() emas? Mijoz {"matn": 5} yuborsa, (5).trim() — TypeError va server 500 qaytaradi. Biz esa "satr bo'lishi kerak" (422) xabarini kutamiz. Transform qiymatni tanimasa — o'zgarishsiz qaytarsin, tur xatosini validator bersin.

Tekshirib ko'ring: @Transform(({ value }) => value.toLowerCase()) yozilgan email maydoniga {"email": ["a@b.uz"]} keldi. Nima bo'ladi?

Javob

TypeError: value.toLowerCase is not a function — massivda bunday metod yo'q. Xato plainToInstance ichida, tekshiruvdan oldin chiqadi, shuning uchun mijoz 400 emas, 500 oladi. To'g'risi — avval turini tekshirish: typeof value === "string" ? value.toLowerCase() : value.

7. Nest'da: ValidationPipe bilan birinchi uchrashuv

Endi bu hammasini Nest o'zi bajaradi. Ilovaga global pipe qo'shiladi, controller esa DTO turini yozadi:

ts
// main.ts
app.useGlobalPipes(new ValidationPipe());

// controller
@Post('yangi')
yangi(@Body() b: YangiDto) {
  return b;
}

ValidationPipe parametr turini (design:paramtypes dan YangiDto) oladi, plainToInstance va validate ni bajaradi. Xato bo'lsa — 400. Kanon tajribasi bitta controller'ni pipe'siz va sukut ValidationPipe() bilan yurgizgan (natijalar Vitest ichida olingan, NestJS 12.2.0):

text
POST /yangi {"matn":5}
  pipe YO'Q                    → 201 {"qiymat":{"matn":5},"klass":"Object",…}
  new ValidationPipe() — sukut → 400 {"message":["matn must be longer than or equal to 1 and shorter than or equal to 100 characters","matn must be a string"],"error":"Bad Request","statusCode":400}

Pipe'siz {"matn": 5} handler'ga yetdi. Pipe bilan — 400 va message massivida ikkala buzilgan qoida. Xabarlar inglizcha va Nest'ning o'z formatida (statusCode, error) — bizning Problem Details emas. Bu formatni o'zgartirish yo'li — keyingi darsda.

Endi yomon xabar. Xuddi shu tajribada:

text
POST /yangi {"matn":"Non","admin":true}
  new ValidationPipe() — sukut → 201 {"qiymat":{"matn":"Non","admin":true},…}

Sukut sozlamada admin: true o'tib ketdi. DTO'da yo'q maydon tekshirilmaydi ham, olib tashlanmaydi ham. Bu — ValidationPipe ning eng xavfli sukuti, keyingi darsning birinchi mavzusi.

8. Hujumchi nigohi

DTO — tashqi dunyo ma'lumotining birinchi to'sig'i. Hujumchi nimani sinaydi:

  1. Ortiqcha maydon. {"matn":"x","admin":true,"egasi_id":1}. Sukut ValidationPipe ularni o'tkazadi; controller body ni to'g'ridan omborga bersa — ommaviy tayinlash. Himoya — whitelist + forbidNonWhitelisted (keyingi dars) va har amalga alohida DTO.
  2. Tur aralashtirish. {"matn": {"$ne": 1}}, {"matn": ["a"]}. Himoya — har maydonda tur dekoratori (@IsString) va @Transform ichida tur tekshiruvi (aks holda 500).
  3. Ulkan kirish. 100 000 elementli massiv yoki 10 MB satr. Himoya — @ArrayMaxSize, @Length/@MaxLength va tana hajmi chegarasi (express.json({ limit }), 25-qism).
  4. Sekin regex. @Matches(/^(a+)+$/) — ReDoS (Qo'lda routing va query parametrlar). Himoya — oddiy, chegaralangan naqshlar ({1,30}) va uzunlikni regexdan oldin tekshirish (dekorator tartibi — pastda).
  5. Qoida chetlab o'tish. Nomsiz constraint klassi (yuqoridagi limit=0) yoki @IsOptional + null. Himoya — chegaraviy qiymatlar uchun testlar.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Noto'g'ri ma'lumot jim o'tadi DTO interfeys yoki type DTO — klass
an unknown value was passed to the validate function validate() ga oddiy obyekt avval plainToInstance
{} uchun "uzunlik" xabari tur dekoratori tepada @IsString() eng pastga
TypeError: value.trim is not a function (500) @Transform tur tekshirmaydi typeof value === "string" ? … : value
Bir qoida o'rniga boshqasi ishlaydi constraint klasslari nomi bir xil @ValidatorConstraint({ name }) noyob
NaN handler'ga yetadi @Type(() => Number) bor, @IsInt yo'q aylantirishdan keyin tur tekshiruvi

10. Mashqlar

Mashqlar kurs/mashqlar/30/15-dto/ da.

1-mashq (oson): dekoratorni tanlang

  • Maydon email bo'lsin — @()
  • Qiymat faqat "csv" yoki "json" — @(["csv", "json"])
  • Maydon bo'lmasa, qolgan qoidalar tekshirilmasin — @()
  • Massivning har elementi satr — @IsString({ : true })
  • JSON obyektni DTO nusxasiga aylantiradigan funksiya —
Yechim
  • IsEmail — format dekoratori; xabar ikkinchi argumentda: @IsEmail({}, { message }).
  • IsIn — ro'yxatdagi qiymatlar; TypeScript enum bo'lsa — @IsEnum.
  • IsOptional — undefined va null ikkalasida ham o'tkazib yuboradi.
  • each — { each: true } har dekoratorda ishlaydi.
  • plainToInstance — class-transformer'dan; usiz validate() qoidalarni topmaydi.

2-mashq (o'rta): IsOptional ni modelga qo'shing

Darsdagi qoidalar modelini dekoratorsiz shaklda oling: qoidalar oddiy massivda, { maydon, nomi, tekshir } ko'rinishida. tekshir(qoidalar, qiymat) funksiyasini yozing. Qoidalar ro'yxatida { maydon, nomi: "ixtiyoriy" } bo'lsa va maydon qiymati undefined yoki null bo'lsa — shu maydonning qolgan hamma qoidasi o'tkazib yuborilsin (class-validator'dagi kabi). Ishora: avval ixtiyoriy maydonlarni Set ga yig'ing (Set), filter bilan.

Yechim
js
function tekshir(qoidalar, qiymat) {
  const ixtiyoriy = new Set(
    qoidalar
      .filter((q) => q.nomi === "ixtiyoriy")
      .map((q) => q.maydon),
  );
  const xatolar = [];
  for (const q of qoidalar) {
    if (q.nomi === "ixtiyoriy") continue;
    const v = qiymat[q.maydon];
    if (ixtiyoriy.has(q.maydon) && (v === undefined || v === null)) {
      continue;
    }
    if (!q.tekshir(v)) xatolar.push(`${q.maydon}: ${q.nomi}`);
  }
  return xatolar;
}

const satr = (v) => typeof v === "string";
const qoidalar = [
  { maydon: "matn", nomi: "satr", tekshir: satr },
  { maydon: "ism", nomi: "ixtiyoriy" },
  { maydon: "ism", nomi: "satr", tekshir: satr },
];
console.log(tekshir(qoidalar, { matn: "Non" }));
console.log(tekshir(qoidalar, { matn: "Non", ism: null }));
console.log(tekshir(qoidalar, { matn: 5, ism: 7 }));

Konsolda:

text
[]
[]
[ 'matn: satr', 'ism: satr' ]

Ikkinchi qator — aynan @IsOptional ning null tuzog'i: ism: null hech qanday xatosiz o'tdi. Agar null ni rad etish kerak bo'lsa, shartdan v === null ni olib tashlash yetadi — class-validator'da buning o'rniga @ValidateIf((o) => o.ism !== undefined) yoziladi («Loyiha qadami» bo'limidagi OzgarishDto ga qarang).

3-mashq (qiyin): xato daraxtini tekislash

Ichma-ich DTO'da ValidationError daraxt bo'ladi: children ichida yana xatolar. Mijozga esa tekis ro'yxat kerak: { joy: "body.vazifalar.1.matn", xabar: "..." } (25-qismdagi joy formati). yoy(xatolar, yol) funksiyasini yozing: har xato uchun joy — yol + . + property; constraints dagi har xabar — alohida yozuv; children bo'lsa — o'sha joy bilan rekursiv (Rekursiya asoslari).

Yechim
js
function yoy(xatolar, yol, natija = []) {
  for (const x of xatolar) {
    const joy = `${yol}.${x.property}`;
    for (const xabar of Object.values(x.constraints ?? {})) {
      natija.push({ joy, xabar });
    }
    if (x.children?.length) yoy(x.children, joy, natija);
  }
  return natija;
}

const daraxt = [
  {
    property: "vazifalar",
    children: [
      {
        property: "1",
        children: [
          {
            property: "matn",
            constraints: { isLength: "matn 1–100 belgi" },
          },
        ],
      },
    ],
  },
  {
    property: "ortiqcha",
    constraints: { whitelistValidation: "..." },
  },
];
for (const x of yoy(daraxt, "body")) console.log(x.joy, "—", x.xabar);

Konsolda:

text
body.vazifalar.1.matn — matn 1–100 belgi
body.ortiqcha — ...

Kanon tajribasidagi DtoPipe aynan shu funksiyani ishlatadi, bitta qo'shimcha bilan: whitelistValidation qoidasining xabari noma'lum parametr: <maydon> ga almashtiriladi — Zod variantidagi xabarga mos bo'lishi uchun. Pipe'ning o'zini keyingi darsda ko'ramiz.

11. Loyiha qadami: class-validator varianti (loyihaga kirmaydi)

11.1 Kanon qarori

vazifalar-api da kirish tekshiruvi Zod'da qoladi — 25-qismdagi o'sha sxemalar (src/vazifalar/sxema.ts) o'zgarmaydi. Nega — Zod va Standard Schema NestJS'da darsida o'lchov bilan ko'ramiz: tezlik, bog'liqliklar, "bitta manba".

Lekin kanon class-validator'ni chetga surib qo'ymagan. Xuddi shu endpointlar uchun to'liq ishlaydigan variant yozilgan va 25–27-qismlarning HTTP testlari shu variant bilan ham yurgizilgan. Natija (kanon o'lchovi, IlmHamroh, 2026-10):

text
 Test Files  13 passed (13)
      Tests  52 passed (52)

Ya'ni class-validator bu loyihaga yaraydi. Kanon tanlovi — o'lchab asoslangan muqobil, class-validator "noto'g'ri" degani emas.

11.2 Variantni o'zingiz sinash

Variant loyihaning nest tarmog'iga (Nest ko'chishi shu tarmoqda boradi) kirmaydi. Sinab ko'rmoqchi bo'lsangiz — nest dan ajratilgan alohida git tarmog'ida:

bash
git switch -c tajriba/class-validator nest
npm install -E class-validator@0.15.1 class-transformer@0.5.1

nest tarmog'iga qaytish — git switch nest. Keyingi darslarda shu tarmoqda pipe va ichma-ich DTO'lar qo'shiladi.

11.3 vazifa.dto.ts

Fayl joyi — src/vazifalar/dto/vazifa.dto.ts. Xabarlar Zod sxemasidagi bilan aynan bir xil yozilgan — shuning uchun o'sha testlar ikkala variantda o'tadi. Kanon faylida qatorlar uzun edi — telefon uchun bo'ldik va izohlarni qisqartirdik (kod ma'nosi aynan). Bo'lingan nusxani tsc --noEmit bilan tekshirdik.

ts
// vazifa.dto.ts — 25-qism Zod sxemalarining class-validator +
// class-transformer shakli (tajriba varianti). Xabarlar Zod'dagi
// bilan bir xil — o'sha HTTP testlari shu variant bilan ham o'tadi
import { Transform } from "class-transformer";
import {
  IsBoolean, IsIn, IsOptional, IsString, Length, ValidateIf,
} from "class-validator";
import type { TransformFnParams } from "class-transformer";
import { MAKS_MATN } from "../sxema.ts";

/** Zod `.trim()` o'rnida: faqat satr bo'lsa (aks holda
 * IsString xatosi chiqsin) */
const kesilgan = ({ value }: TransformFnParams): unknown =>
  typeof value === "string" ? value.trim() : value;
const MATN_XABARI =
  `matn 1–${String(MAKS_MATN)} belgi bo'lishi kerak`;

/** POST /v1/vazifalar */
export class YangiVazifaDto {
  @Transform(kesilgan)
  // TUZOQ: dekoratorlar PASTDAN YUQORIGA bajariladi — tur
  // tekshiruvi (IsString) eng pastda turadi
  @Length(1, MAKS_MATN, { message: MATN_XABARI })
  @IsString({ message: "matn — satr bo'lishi kerak" })
  matn!: string;

  @IsOptional()
  @IsBoolean({ message: "bajarildi — true yoki false" })
  bajarildi?: boolean;
}

/** PUT /:id — hamma yoziladigan maydon majburiy */
export class ToliqVazifaDto {
  @Transform(kesilgan)
  @Length(1, MAKS_MATN, { message: MATN_XABARI })
  @IsString({ message: "matn — satr bo'lishi kerak" })
  matn!: string;

  @IsBoolean({ message: "bajarildi — true yoki false" })
  bajarildi!: boolean;
}

/**
 * PATCH (Merge Patch): ikkala maydon ixtiyoriy, lekin `null` —
 * xato. TUZOQ: @IsOptional() null'ni ham "yo'q" deb o'tkazadi —
 * shuning uchun ValidateIf(!== undefined). "Kamida bitta maydon" —
 * obyekt darajasidagi qoida yo'q: controller tekshiradi
 */
export class OzgarishDto {
  @ValidateIf((o: OzgarishDto) => o.matn !== undefined)
  @Transform(kesilgan)
  @Length(1, MAKS_MATN, { message: MATN_XABARI })
  @IsString({ message: "matn — satr bo'lishi kerak" })
  matn?: string;

  @ValidateIf((o: OzgarishDto) => o.bajarildi !== undefined)
  @IsBoolean({ message: "bajarildi — true yoki false" })
  bajarildi?: boolean;
}

/** POST /v1/eksportlar — Zod discriminatedUnion o'rnida.
 * "json + ajratgich" taqiqi — obyekt darajasidagi qoida,
 * controller'da */
export class EksportDto {
  @IsOptional()
  @IsIn(["csv", "json"], {
    message: "format — \"csv\" yoki \"json\"",
  })
  format?: "csv" | "json";

  @IsOptional()
  @IsIn([",", ";"], { message: "ajratgich — \",\" yoki \";\"" })
  ajratgich?: "," | ";";
}

Uchta joyni Zod sxemasi bilan solishtiring:

  • ValidateIf. Zod'da Matn.optional() — undefined ni o'tkazadi, null ni rad etadi. class-validator'da @IsOptional() ikkalasini o'tkazadi, shuning uchun PATCH'da @ValidateIf((o) => o.matn !== undefined) — "faqat maydon berilgan bo'lsa tekshir".
  • "Kamida bitta maydon". Zod'da .refine(...) sxemaning o'zida. class-validator'da obyekt darajasidagi qoida yo'q — bu tekshiruv controller'ga ko'chdi.
  • Eksport. Zod'da discriminatedUnion: csv bo'lsa ajratgich mumkin, json bo'lsa — yo'q. DTO'da ikki mustaqil ixtiyoriy maydon qoldi, "json + ajratgich" taqiqi yana controller'da.

Shu uch joy — class-validator'ning tabiiy chegarasi: qoidalar maydonga bog'langan. Maydonlar orasidagi qoida uchun o'z validator klassi yoki controller kodi kerak.

Hali commit qilmang. nest tarmog'ida bu darsda o'zgarish yo'q; loyihaning keyingi commit'i Javobni serializatsiya qilish darsida bo'ladi.

12. Real ishda

  • Nest loyihalarining aksariyati — src/<modul>/dto/create-*.dto.ts, update-*.dto.ts va global ValidationPipe. Nest hujjatidagi "Validation" bo'limi aynan shu yo'l bilan yozilgan.
  • Review'dagi savollar: "bu DTO interfeys emasmi?", "@IsString pastdami?", "@Transform turini tekshiradimi?", "massivda @ArrayMaxSize bormi?".
  • Intervyu: "DTO va entity farqi nima?", "nega DTO klass bo'lishi kerak?", "plainToInstance nima qiladi?", "o'z validatorni qanday yozasiz?".
  • Ehtiyot: class-validator'ga qo'shimcha paket — class-transformer — 2021-yildan beri yangi versiya chiqarmagan (npm ma'lumoti, 2026-10). Bu "ishlamaydi" degani emas, lekin uzoq muddatga tanlashda e'tiborga olinadi.

Xulosa

  • DTO — mijoz yuboradigan ma'lumot shakli; har amalga alohida, entity'dan ajratilgan; klass, interfeys emas.
  • class-validator ikki qadamda: plainToInstance (nusxa + @Transform/@Type) → validate() → ValidationError[] (property, value, constraints, children).
  • Dekoratorlar pastdan yuqoriga qo'llanadi: tur tekshiruvi eng pastda.
  • O'z qoida — @ValidatorConstraint({ name }) + @Validate yoki registerDecorator; nom noyob bo'lsin.
  • @IsOptional null ni ham o'tkazadi; @Transform ichida tur tekshiring.
  • Kanon class-validator variantini to'liq yozgan (52/52 test), lekin loyihada Zod qoladi.

Keyingi dars: ValidationPipe sozlamalari va tuzoqlari — whitelist, forbidNonWhitelisted, transform, enableImplicitConversion ("false" → true!), exceptionFactory va pipe'ni global yoki lokal ulash.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
DTO va class-validator NestJS'da: dekoratorli qoidalar, ValidationError va o'z validatorlar — IlmHamroh