IlmHamroh
JavaScript Full-stack/30-qism. NestJS19/65-dars24 daqiqa
Mundarija (31)

Zod va Standard Schema NestJS'da: @Body({ schema }), nestjs-zod va o'lchov

Qisqacha: NestJS 12 da parametr dekoratoriga sxema beriladi: @Body({ schema: YangiVazifaSxemasi }), @Param("id", { schema: IdSxemasi }). Sxema — Standard Schema obyekti (Zod, Valibot, ArkType); uni pipe metadata.schema dan oladi va ~standard.validate() bilan tekshiradi. Natija — sxemaning chiqishi: "7" → 7, " Non " → "Non". Tayyor pipe — StandardSchemaValidationPipe; eski yo'l — nestjs-zod paketining createZodDto. vazifalar-api 25-qismdagi Zod sxemalarini o'zgarishsiz ishlatadi. Kanon o'lchovida 6 parametrli ro'yxat so'rovida class-validator varianti Zod variantining ~45 % tezligida ishladi.

Bu darsda

  • @Body/@Query/@Param({ schema }) yozuvi qanday ishlashini va pipe sxemani qayerdan olishini tushunasiz.
  • O'rnatilgan StandardSchemaValidationPipe va nestjs-zod ni haqiqiy javoblarda ko'rasiz.
  • class-validator va Zod'ni to'rt o'lchov bo'yicha solishtirasiz: tezlik, kod hajmi, bog'liqliklar, xulq.
  • vazifalar-api controller'larini sxemali parametrlarga o'tkazasiz va bitta kutilgan test xatosini o'qiysiz.

Oldin bilishingiz kerak: Zod 4 serverda, Backend validatorlari: Joi, class-validator va express-validator, Pipe'lar, Mapped types va nested validatsiya.

O'tgan darsda SxemaPipe ni yozdik: u metadata.schema ni o'qiydi. Bu darsda o'sha schema qayerdan kelishini ko'ramiz va pipe'ni ishga tushiramiz.

1. Nega bu kerak?

Uch dars davomida class-validator'ni o'rgandik — Nest dunyosining standart yo'li. vazifalar-api da esa 25-qismdan beri Zod sxemalari bor: YangiVazifaSxemasi, RoyxatQuerySxemasi, IdSxemasi. Ular sinalgan, testlari bor, frontend ham Zod'da.

Savol: Nest'da Zod'ni qanday ishlatamiz? Hozirgi controller'lar uni handler ichida chaqiradi — tekshir(YangiVazifaSxemasi, req.body, "body"). Bu Express usuli: har handlerda takror, @Req() ga bog'langan, tekshiruv handler'ning birinchi qatorlarida "yashiringan".

NestJS 12 gacha bu savolga jamiyat paketlari javob berardi (nestjs-zod). NestJS 12 da Nest'ning o'zi Standard Schema ni qo'llay boshladi: sxema to'g'ridan-to'g'ri parametr dekoratoriga yoziladi. Ya'ni Zod endi "ruxsat etilgan muqobil" emas, Nest'ning birinchi darajali yo'li.

Sxemalar o'zgarmaydi — o'zgaradigan narsa faqat chaqiriladigan joy:

Express va hozirgi controller Shu darsdan keyin
Sxema YangiVazifaSxemasi (25-qism) o'sha
Kim chaqiradi handler: tekshir(sxema, req.body, "body") global SxemaPipe
Sxema qayerda yoziladi handler ichida parametr dekoratorida: @Body({ schema })
Handler nima oladi req tayyor, aylantirilgan qiymat

Yutuq — handler'lar qisqaradi va @Req() ga bog'liqlik kamayadi. Narx — tekshiruv tartibi o'zgaradi; buning bitta kutilgan oqibatini «Loyiha qadami» da ko'ramiz. Ya'ni bu dars yangi validatsiya kutubxonasini o'rgatmaydi. U bor sxemalarni Nest'ning so'rov yo'liga to'g'ri joyga ko'chiradi.

O'xshatish: elektr vilkasi. Har davlatning o'z rozetkasi bo'lsa, har asbobga adapter kerak. Standard Schema — umumiy rozetka standarti: Zod, Valibot, ArkType — har biri o'z "asbobi", lekin vilkasi bir xil (~standard). Nest rozetkani bir marta o'rnatdi — qaysi kutubxonani ulasangiz ham ishlaydi.

2. Standard Schema — qisqa takror

Backend validatorlari darsida ko'rganimiz: Standard Schema'ni qo'llaydigan har sxemada ~standard xususiyati bor. Unda vendor (kutubxona nomi) va validate(qiymat) metodi. validate natijasi — yo { value }, yo { issues }.

Ikkita turli kutubxona — bitta interfeys:

ts
import { z } from "zod";
import * as v from "valibot";

const zodSxema = z.object({ matn: z.string().min(1) });
const valibotSxema = v.object({
  matn: v.pipe(v.string(), v.minLength(1)),
});

for (const s of [zodSxema, valibotSxema]) {
  const yomon = await s["~standard"].validate({ matn: "" });
  const yaxshi = await s["~standard"].validate({ matn: "Non" });
  console.log(
    s["~standard"].vendor,
    yomon.issues?.length,
    JSON.stringify(yaxshi.issues === undefined ? yaxshi.value : null),
  );
}

Konsolda:

text
zod 1 {"matn":"Non"}
valibot 1 {"matn":"Non"}

Kod zodSxema va valibotSxema farqini bilmaydi — faqat ~standard ni chaqiradi. SxemaPipe aynan shunday yozilgan: unda zod importi yo'q.

3. @Body({ schema }) — NestJS 12

3.1 Sxema qayerga yoziladi

ts
@Post(':id')
yarat(
  @Param('id', { schema: Id }) id: number,
  @Body({ schema: YangiVazifa }) body: YangiVazifa,
) { … }

Yangi shakl: dekoratorga obyekt beriladi — { schema }, kerak bo'lsa { schema, pipes: [...] }. Nomli parametr (@Param("id", ...)) uchun obyekt ikkinchi argument.

Dekorator o'zi hech narsani tekshirmaydi. U sxemani marshrut metama'lumotiga — parametr yozuvining yoniga — qo'yib qo'yadi. So'rov kelganda Nest har parametr uchun pipe'larni chaqiradi va ArgumentMetadata.schema ga o'sha sxemani beradi:

sequenceDiagram
  participant D as "@Body({ schema })"
  participant N as Nest router
  participant P as Pipe
  participant H as Handler
  D->>N: ishga tushishda: sxema metama'lumotga
  N->>P: transform(tana, { type: "body", schema })
  P->>P: schema["~standard"].validate(tana)
  P-->>N: value (yoki xato)
  N->>H: yarat(id, value)

Diagrammaning birinchi qatoriga qarang: sxema ilova ishga tushganda bir marta yoziladi; har so'rovda faqat pipe ishlaydi. Pipe bo'lmasa — sxema bor, lekin hech kim uni tekshirmaydi.

3.2 O'rnatilgan StandardSchemaValidationPipe

NestJS 12 tayyor pipe beradi. Demo loyihada Zod sxemalari bilan sinadik (biz ishga tushirdik: NestJS 12.2.0, zod 4.6.7, Node 24.21.0):

ts
const YangiVazifa = z.strictObject({
  matn: z.string().trim().min(1).max(100),
  bajarildi: z.boolean().optional(),
});
type YangiVazifa = z.infer<typeof YangiVazifa>;
const Id = z.string().regex(/^[1-9]\d*$/).transform(Number);

@Controller()
class C {
  @Post('nest/:id')
  nest(
    @Param('id', { schema: Id }) id: number,
    @Body({ schema: YangiVazifa }) body: YangiVazifa,
  ) {
    return { id, turi: typeof id, body };
  }
}

app.useGlobalPipes(new StandardSchemaValidationPipe());
text
POST /nest/7 {"matn":"  Non olish  "}
  → 201 {"id":7,"turi":"number","body":{"matn":"Non olish"}}
POST /nest/7 {"matn":"","admin":true}
  → 400 {"message":["matn: Too small: expected string to have >=1 characters","Unrecognized key: \"admin\""],"error":"Bad Request","statusCode":400}
POST /nest/07 {"matn":"x"}
  → 400 {"message":["Invalid string: must match pattern /^[1-9]\\d*$/"],"error":"Bad Request","statusCode":400}

Uch narsaga qarang:

  • Aylantirish sxemaning o'zida. id — 7 soni (transform(Number)), matn — kesilgan. Handler sxemaning chiqishini oladi. class-validator'dagi "tekshirish — bir joyda, aylantirish — boshqa joyda" muammosi yo'q.
  • "07" rad etildi — regex tekshiruvi aylantirishdan oldin. O'tgan ikki darsdagi "01" → 200 xatosi bu yerda yuz bermaydi.
  • Ortiqcha maydon — z.strictObject tufayli "Unrecognized key". whitelist sozlamasi kerak emas — qoida sxemaning o'zida.

Xabarlar inglizcha — Zod'ning sukut tili. vazifalar-api da z.config(z.locales.uz()) yoqilgan, shuning uchun u yerda o'zbekcha chiqadi.

3.3 Nega kanon o'rnatilgan pipe'ni ishlatmadi

StandardSchemaValidationPipe ning sozlamalari: errorHttpStatusCode (sukut 400) va exceptionFactory(issues). vazifalar-api ga esa tana xatosi uchun 422, query va yo'l uchun 400 kerak. exceptionFactory faqat issues ni oladi — xato tanadanmi yoki query'danmi, bilmaydi. ValidationPipe sozlamalari darsidagi o'sha muammo. Shuning uchun kanon o'z SxemaPipe ini yozdi: 30 qator va metadata.type bo'yicha bitta if.

Tekshirib ko'ring: Global pipe sifatida StandardSchemaValidationPipe ulangan. Controller'da @Body() body: YangiVazifa yozilgan — schema yo'q. Mijoz {"matn": 5} yuborsa nima bo'ladi?

Javob

O'tadi. Pipe metadata.schema ni qidiradi — yo'q, demak tekshiradigan narsa yo'q, qiymat o'zgarishsiz uzatiladi. YangiVazifa — TypeScript turi, u ish vaqtida yo'q (DTO va class-validator darsidagi interfeys tuzog'ining o'zi). Sxemali yo'lda har parametrga { schema } yozish shart — buni review va testlar nazorat qiladi.

4. nestjs-zod: eski yo'l

4.1 createZodDto

NestJS 12 gacha Zod bilan ishlashning eng mashhur yo'li — nestjs-zod paketi. U Zod sxemasidan DTO klassi yasaydi: class YangiVazifaDto extends createZodDto(YangiVazifa) {}. Klass bor — demak @Body() body: YangiVazifaDto yoziladi va paketning ZodValidationPipe i metatype dan sxemani topadi. Afzalligi — klass orqali Swagger hujjati ham yasaladi.

4.2 NestJS 12 bilan

Demo loyihaga o'rnatishga urindik (2026-11-02, nestjs-zod 5.5.0 — npm'dagi eng yangi):

bash
npm install --save-exact nestjs-zod@5.5.0
text
npm error Found: @nestjs/common@12.2.0
npm error node_modules/@nestjs/common
npm error   @nestjs/common@"12.2.0" from the root project
npm error
npm error Could not resolve dependency:
npm error peer @nestjs/common@"^10.0.0 || ^11.0.0" from nestjs-zod@5.5.0
npm error node_modules/nestjs-zod
npm error   nestjs-zod@"5.5.0" from the root project

Tarjimasi: "Bog'liqlikni hal qilib bo'lmadi: nestjs-zod@5.5.0 @nestjs/common ning 10 yoki 11-versiyasini talab qiladi (peer), loyihada esa 12.2.0". Semver darsidagi ^11.0.0 — "11.x.x, lekin 12 emas". Paket hali NestJS 12 ni rasman qo'llamaydi.

Buni o'rnatishdan oldin ham bilish mumkin edi. npm view buyrug'i paketning peer bog'liqliklarini ko'rsatadi:

bash
npm view nestjs-zod@5.5.0 peerDependencies

Biz 2026-11-02 da tekshirdik — javobda to'rt qator: @nestjs/common uchun ^10.0.0 || ^11.0.0, @nestjs/swagger uchun ^7.4.2 || ^8.0.0 || ^11.0.0, rxjs uchun ^7.0.0, zod uchun ^3.25.0 || ^4.0.0. Ikkinchi qatorga ham qarang: Swagger moduli ham faqat 11-versiyagacha. Bizning loyihada @nestjs/swagger 12.0.2 — demak nestjs-zod ning Swagger qismi bilan ham xuddi shu ziddiyat bo'ladi. O'sha kuni npm'dagi eng yangi versiya ham 5.5.0 edi: NestJS 12 ni qo'llaydigan reliz hali chiqmagan.

Tekshirib ko'ring: Paketning peer'i "@nestjs/common": "^11.0.0". Loyihangizda @nestjs/common 11.1.6. npm install xato beradimi? 12.0.0 bo'lsa-chi?

Javob

11.1.6 — yo'q: ^11.0.0 "11.x.x ning istalgani" degani, 11.1.6 shunga kiradi. 12.0.0 — ha, ERESOLVE: katta raqam o'zgargan, ^ uni qamramaydi (Semver). Katta versiya — "API buzilishi mumkin" degan belgi, shuning uchun paket muallifi 12 ni sinamaguncha qo'shmaydi.

--legacy-peer-deps bayrog'i bilan o'rnatdik (npm peer tekshiruvini o'tkazib yuboradi) va sinadik:

ts
class YangiVazifaDto extends createZodDto(YangiVazifa) {}

@Post('nestjs-zod')
nz(@Body() body: YangiVazifaDto) {
  return { body, klass: body.constructor.name };
}
// main.ts: app.useGlobalPipes(new ZodValidationPipe());
text
POST /nestjs-zod {"matn":"  Non  "}
  → 201 {"body":{"matn":"Non"},"klass":"Object"}
POST /nestjs-zod {"matn":5}
  → 400 {"statusCode":400,"message":"Validation failed","errors":[{"expected":"string","code":"invalid_type","path":["matn"],"message":"Invalid input: expected string, received number"}]}

Ishladi: kesish, tur xatosi. Xato formati boshqacha — errors massivida Zod'ning xom issues lari. klass — Object: DTO klassi faqat sxemani tashuvchi, handler esa oddiy obyekt oladi.

Xulosa: bizning bitta sinovimizda ishladi, lekin rasmiy qo'llanmagan versiyani prod'ga qo'yish — xavf. NestJS 12 da o'rnatilgan { schema } yo'li bu paketning asosiy vazifasini qamraydi. nestjs-zod hali kerak bo'ladigan joy — Swagger: lekin vazifalar-api Swagger'ni Zod sxemalaridan to'g'ridan-to'g'ri yasaydi (Swagger).

Diqqat: --legacy-peer-deps — muammoni yashiradi, hal qilmaydi. Uni package.json skriptlariga yoki .npmrc ga doimiy yozib qo'ysangiz, keyingi barcha peer ziddiyatlari ham jim o'tadi.

5. Nega kanonda Zod: o'lchov

Kanon ikkala variantni — Zod va class-validator — xuddi shu endpointlar bilan to'liq yozib, o'lchagan (kanon o'lchovi, IlmHamroh, 2026-10). Ikkalasi ham 25–27 testlarining 52 tasidan o'tgan. Farqlar to'rt o'qda.

5.1 Tezlik: HTTP

Har endpointga yuk berildi (autocannon, 10 ulanish, 5 aylana × 3 soniya, variantlar navbat bilan; mediana). Mashinada bir vaqtda boshqa ishlar ham bo'lgan — mutlaq req/s emas, nisbat muhim. Zod varianti = 100 %:

class-validator varianti tezligi (Zod = 100 %)
  • Zod (asos)100 %
  • GET ro'yxat (6 parametr)44,8 %
  • 422 xato yo'li75,2 %
  • PATCH (yozuv)78 %
  • GET /:id107,2 %

Manba: kanon o'lchovi (IlmHamroh, 2026-10), autocannon mediana, bitta mashina, navbat bilan; NestJS 12.2.0, Node 24.21.0

  • GET ro'yxat — eng katta farq: 2 544 va 1 140 req/s, ya'ni Zod ~2.2 marta tez. Bu so'rovda 6 ta query parametri tekshiriladi va aylantiriladi — validatsiya ishi ko'p.
  • PATCH va 422 yo'li — class-validator 75–78 %.
  • GET /:id — farq yo'q (107 % — o'lchov shovqini ichida). Bitta id tekshiruvi so'rov vaqtida sezilmaydi.

Ya'ni farq validatsiya ishi ko'p bo'lgan joyda ko'rinadi. Odatiy CRUD'da so'rov vaqtining asosiy qismi baza va tarmoqqa ketadi.

5.2 Tezlik: bitta tekshiruv

Xuddi shu yaroqli tanani 200 000 marta tekshirish (kurs tajribasi, isitishdan keyin; mln/s — soniyada million tekshiruv):

Bitta tanani tekshirish tezligi
  • zod safeParse5,43 mln/s
  • zod ~standard.validate3,13 mln/s
  • plainToInstance + validateSync0,3 mln/s
  • Nest ValidationPipe.transform0,19 mln/s

Manba: kanon tajribasi (IlmHamroh, 2026-10-27), Node 24.21.0, i5-12500H, 200 000 aylana

Zod safeParse — 0.18 µs, Nest ValidationPipe — 5.22 µs: ~27 marta. Standard Schema yo'li (~standard.validate) to'g'ridan safeParse dan sekinroq (Promise qaytaradi), lekin baribir class-validator'dan ~10 marta tez. Bu raqamlar Backend validatorlari darsidagi o'lchov (zod 6.63, class-validator 0.30 mln/s) bilan mos.

Yana bir bor: 5 µs — HTTP so'rovida kichik vaqt. Shuning uchun HTTP o'lchovidagi farq (2.2×) mikro-o'lchovdagidan (27×) ancha kichik.

5.3 Kod hajmi va "bitta manba"

Zod class-validator
Ro'yxat query'si 45 qator sxema 101 qator DTO + aylantirish funksiyasi
TypeScript turi z.infer — sxemadan DTO klassi = tur, aylantirilgan tur alohida
JSON Schema z.toJSONSchema — sxemadan alohida vosita kerak
Obyekt qoidalari .refine() sxemada controller'da
Frontend bilan o'sha sxema alohida

class-validator'da bitta qoida uch joyda yashaydi: dekorator (tekshiruv), @Transform yoki aylantirish funksiyasi (o'zgartirish), TypeScript turi. Zod'da — bitta zanjir. vazifalar-api da bu ayniqsa muhim: frontend (15–22-qismlar) va backend bir xil sxema tilida gaplashadi.

5.4 Bog'liqliklar

Paket Oxirgi versiya (npm, 2026-10)
zod 4.6.7 — loyihada 25-qismdan bor
class-validator 0.15.1 — 2026-02-26
class-transformer 0.5.1 — 2021-11-22

Zod qolsa — prod bog'liqlik qo'shilmaydi. class-validator yo'li ikki yangi paket qo'shadi, ulardan biri besh yildan beri yangilanmagan. Backend validatorlari darsida o'lchangan hajm ham shu yerga: class-validator + class-transformer + reflect-metadata — 16.7 MB.

5.5 Xulq

33 ta noto'g'ri so'rovdan 22 tasida javoblar aynan bir xil, 11 tasida farq (Mapped types va nested validatsiya darsida ko'rdik). Ikkita haqiqiy xulq farqi (/01 → 200, /1e3 → 404) — class-validator varianti tomonida.

5.6 Qaror

Kanon Zod'ni qoldirdi: tezroq, bitta manba, yangi bog'liqliksiz va xulqi 25-qism shartnomasiga aniqroq mos. Muqobil ham tayyor: class-validator varianti to'liq yozilgan va loyihani unga o'tkazish — 519 qatorlik o'zgarish.

Qachon class-validator to'g'ri tanlov:

  • jamoa va mavjud kod class-validator'da — ikki xil validatorni aralashtirmang;
  • Nest CLI Swagger plagini DTO klasslaridan hujjatni avtomatik yasashi kerak;
  • frontend Zod ishlatmaydi va "bitta manba" afzalligi yo'q.

Ya'ni bu "class-validator yomon" degani emas — o'lchab tanlangan muqobil.

6. Hujumchi nigohi

  1. z.object va z.strictObject. z.object noma'lum kalitlarni jim olib tashlaydi — whitelist kabi. z.strictObject — xato beradi, forbidNonWhitelisted kabi. Kanon sxemalarining hammasi strictObject.
  2. z.coerce — implicit conversion'ning Zod'dagi egizagi. z.coerce.boolean() ichida Boolean(qiymat) — "false" → true. Kanon z.coerce ishlatmaydi — query mantiqiy qiymatlari aniq "true"/"false" bilan (blok pastda).
  3. Sxemasiz parametr. { schema } yozish unutilsa — parametr tekshiruvsiz o'tadi («Nega kanon o'rnatilgan pipe'ni ishlatmadi» bo'limidagi savol). @Req() orqali req.body ni to'g'ridan o'qish ham shunday. Himoya — har yozuvchi endpoint uchun "noto'g'ri tana → 422" testi.
  4. Peer ziddiyatini yashirish. --legacy-peer-deps bilan o'rnatilgan paket keyingi yangilanishda jim buzilishi mumkin. Himoya — rasmiy qo'llanadigan versiya yoki o'rnatilgan yo'l.

z.coerce tuzog'ini brauzerda tekshiring:

ts
import { z } from "zod";

console.log(z.coerce.boolean().parse("false"));
const Mantiqiy = z.enum(["true", "false"])
  .transform((x) => x === "true");
console.log(Mantiqiy.parse("false"));
console.log(Mantiqiy.safeParse("ha").success);

Konsolda:

text
true
false
false

Birinchi qator — z.coerce ning "false" → true xatosi. Keyingi ikkitasi — aniq sxema: "false" → false, "ha" — rad.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Sxema bor, lekin tekshirilmaydi global pipe ulanmagan StandardSchemaValidationPipe yoki o'z pipe
Ortiqcha maydon jim yo'qoladi z.object z.strictObject
"false" → true z.coerce.boolean() z.enum(["true","false"]).transform(...)
npm error ERESOLVE ... peer @nestjs/common paket Nest 12 ni qo'llamaydi o'rnatilgan { schema } yo'li
422 o'rniga 400 StandardSchemaValidationPipe bitta status o'z pipe (metadata.type)
Handler'da id satr sxemada .transform(Number) yo'q sxema chiqishi = handler kirishi

8. Mashqlar

Mashqlar kurs/mashqlar/30/19-standard-schema/ da.

1-mashq (oson): bo'shliqni to'ldiring

  • Tanaga sxema berish — @Body({ : YangiVazifaSxemasi })
  • Pipe sxemani oladigan joy — metadata.
  • Standard Schema metodi — sxema["~standard"].(qiymat)
  • Noma'lum kalitda xato beradigan Zod obyekti — z.
  • NestJS 12 ning tayyor pipe'i —
Yechim
  • schema — ParameterDecoratorOptions ning maydoni; nomli parametrda ikkinchi argument: @Param("id", { schema }).
  • schema — ArgumentMetadata.schema.
  • validate — { value } yoki { issues }; Promise qaytarishi mumkin.
  • strictObject — z.object jim tashlaydi.
  • StandardSchemaValidationPipe — sukut 400, exceptionFactory(issues).

2-mashq (o'rta): SxemaPipe modeli

SxemaPipe ning mantiqini kutubxonasiz takrorlang. Kichik Standard Schema obyektini qo'lda yasang (~standard.validate — matn satr va bo'sh emasmi). pipe(qiymat, metadata) funksiyasi: metadata.schema yo'q bo'lsa — qiymatni o'zgarishsiz qaytarsin; xato bo'lsa { status, xatolar } tashlasin: type === "body" — 422, aks holda 400; joy — body.matn ko'rinishida. Ishora: map va join (Massiv bo'ylab yurish).

Yechim
js
const MatnSxemasi = {
  "~standard": {
    version: 1,
    vendor: "qolda",
    validate(v) {
      if (typeof v?.matn !== "string" || v.matn.trim() === "") {
        const xato = { path: ["matn"], message: "matn bo'sh" };
        return { issues: [xato] };
      }
      return { value: { matn: v.matn.trim() } };
    },
  },
};
const JOY = { body: "body", query: "query", param: "params" };

async function pipe(qiymat, metadata) {
  const joy = JOY[metadata.type];
  if (metadata.schema === undefined || joy === undefined) {
    return qiymat;
  }
  const n = await metadata.schema["~standard"].validate(qiymat);
  if (n.issues === undefined) return n.value;
  const xatolar = n.issues.map((i) => ({
    joy: [joy, ...(i.path ?? [])].join("."),
    xabar: i.message,
  }));
  throw { status: metadata.type === "body" ? 422 : 400, xatolar };
}

const body = { type: "body", schema: MatnSxemasi };
console.log(await pipe({ matn: "  Non  " }, body));
console.log(await pipe({ x: 1 }, { type: "body" }));
try {
  await pipe({ matn: " " }, body);
} catch (x) {
  console.log(x.status, JSON.stringify(x.xatolar));
}
try {
  await pipe({}, { type: "query", schema: MatnSxemasi });
} catch (x) {
  console.log(x.status, JSON.stringify(x.xatolar));
}

Konsolda:

text
{ matn: 'Non' }
{ x: 1 }
422 [{"joy":"body.matn","xabar":"matn bo'sh"}]
400 [{"joy":"query.matn","xabar":"matn bo'sh"}]

Ikkinchi qator — sxemasiz parametrning xavfi: { x: 1 } hech narsasiz o'tdi. Haqiqiy SxemaPipe da ham shunday — shuning uchun { schema } ni unutmaslik testlar bilan nazorat qilinadi.

3-mashq (qiyin): qisman muvaffaqiyat

POST /v1/vazifalar/toplam da har element alohida tekshiriladi va o'z statusini oladi. toplam(elementlar, sxema) funksiyasini yozing: har element uchun sxema bilan tekshiring; yaroqli — { indeks, holat: 201, vazifa: {id, ...} } (id — 1 dan ketma-ket), yaroqsiz — { indeks, holat: 422, xato }. Umumiy status: hammasi 201 bo'lsa — 201, aks holda 207. Oldingi mashqdagi MatnSxemasi ni ishlating (bu yerda sinxron chaqirsa bo'ladi — u Promise qaytarmaydi).

Yechim
js
const MatnSxemasi = {
  "~standard": {
    validate(v) {
      if (typeof v?.matn !== "string" || v.matn.trim() === "") {
        return { issues: [{ message: "matn bo'sh" }] };
      }
      return { value: { matn: v.matn.trim() } };
    },
  },
};

function toplam(elementlar, sxema) {
  let id = 0;
  const natijalar = elementlar.map((el, indeks) => {
    const n = sxema["~standard"].validate(el);
    if (n.issues) {
      const xabar = n.issues.map((i) => i.message).join("; ");
      return { indeks, holat: 422, xato: xabar };
    }
    return { indeks, holat: 201, vazifa: { id: ++id, ...n.value } };
  });
  const hammasi = natijalar.every((n) => n.holat === 201);
  return { status: hammasi ? 201 : 207, natijalar };
}

const javob = toplam([{ matn: "A" }, { matn: "" }, 5], MatnSxemasi);
console.log(javob.status);
for (const n of javob.natijalar) console.log(JSON.stringify(n));
console.log(toplam([{ matn: "B" }], MatnSxemasi).status);

Konsolda:

text
207
{"indeks":0,"holat":201,"vazifa":{"id":1,"matn":"A"}}
{"indeks":1,"holat":422,"xato":"matn bo'sh"}
{"indeks":2,"holat":422,"xato":"matn bo'sh"}
201

Kanon controller'idagi toplam metodi xuddi shu tuzilmada — faqat YangiVazifaSxemasi.safeParse(el) bilan va yaroqlilarni omborga bitta chaqiruvda yozadi. BulkSxemasi esa faqat tashqi qobiqni (1..100 elementli massiv) pipe orqali tekshiradi.

9. Loyiha qadami: sxemali parametrlar

9.1 Nima o'zgaradi

  • Controller'lar: tekshir(...) chaqiruvlari o'rniga @Body/@Query/@Param({ schema }). Sxemalar (src/vazifalar/sxema.ts) o'zgarmaydi.
  • AppModule: SxemaPipe global pipe sifatida ulanadi.
  • Handler'lardan jsonTanaKerak(req) chaqiruvi ham olib tashlanadi (sababi va oqibati — pastda).

9.2 vazifalar.controller.ts

Kanon faylida qatorlar uzun edi — telefon uchun bo'ldik va izohlarni qisqartirdik (kod ma'nosi aynan). Bu — faylning shu darsdagi holati; bo'lingan nusxani tsc --noEmit va ESLint bilan tekshirdik.

ts
// vazifalar.controller.ts — /v1/vazifalar: HTTP ↔ xizmat (25-qism
// marshrut.ts dan). Kirish: Nest 12 `@Body/@Query/@Param({ schema })`
// — 25-qism Zod sxemalari (sxema.ts) o'zgarmadi, tekshiruvni global
// SxemaPipe qiladi. @Res({ passthrough: true }) — faqat sarlavhalar
// uchun
import {
  All, Body, Controller, Delete, Get, HttpCode, Inject, Param, Patch,
  Post, Put, Query, Req, Res,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { MetodYoqXatosi } from "../umumiy/xatolar.ts";
import type { MaydonXatosi } from "../umumiy/xatolar.ts";
import { kursorYoz } from "../umumiy/kursor.ts";
import {
  ifMatch, keshYoq, vazifaSarlavhalari,
} from "../umumiy/shartli.ts";
import { asosYol } from "../umumiy/yol.ts";
import { ILOVA_SOZLAMALARI } from "../tokenlar.ts";
import type { IlovaSozlamalari } from "../tokenlar.ts";
import {
  BulkSxemasi, IdSxemasi, OzgarishSxemasi, RoyxatQuerySxemasi,
  ToliqVazifaSxemasi, ToplamSxemasi, YangiVazifaSxemasi,
} from "./sxema.ts";
import type { RoyxatQuery } from "./sxema.ts";
import type {
  ToliqVazifa, Vazifa, VazifaOzgarishi, YangiVazifa,
} from "./vazifa.ts";
import type { AlmashtirishVazifasi } from "./ombor.ts";
import { proyeksiya, royxatSorovi } from "./sorov.ts";
import { VazifalarService } from "./vazifalar.service.ts";

/** Keyingi sahifa havolasi: query nusxasi, offset/cursor yangi */
function keyingiHavola(
  req: Request,
  ozgartir: Record<string, string>,
): string {
  const p = new URLSearchParams(req.originalUrl.split("?")[1] ?? "");
  p.delete("offset");
  p.delete("cursor");
  for (const [k, v] of Object.entries(ozgartir)) p.set(k, v);
  return `${asosYol(req, "vazifalar")}?${p.toString()}`;
}

type Natija = {
  indeks: number;
  holat: number;
  vazifa?: Vazifa;
  xato?: MaydonXatosi;
};
type Id = { id: number };

// Versiya — ilova.ts dagi enableVersioning (URI): /v1/vazifalar
// va eskirgan /vazifalar
@Controller("vazifalar")
export class VazifalarController {
  constructor(
    private readonly xizmat: VazifalarService,
    @Inject(ILOVA_SOZLAMALARI)
    private readonly sozlamalar: IlovaSozlamalari,
  ) {}

  @Get()
  async royxat(
    @Query({ schema: RoyxatQuerySxemasi }) query: RoyxatQuery,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<object> {
    const { sorov, maydonlar, imzo } =
      royxatSorovi(query, this.sozlamalar.tz);
    const natija = await this.xizmat.royxat(sorov);
    const sahifa: Record<string, unknown> = { limit: sorov.limit };
    let keyingi: string | undefined;
    if (sorov.sahifa.tur === "offset") {
      sahifa.offset = sorov.sahifa.offset;
      if (natija.borKeyingi) {
        const offset = String(sorov.sahifa.offset + sorov.limit);
        keyingi = keyingiHavola(req, { offset });
      }
    } else {
      const kursor =
        natija.keyingi && kursorYoz(natija.keyingi, imzo);
      sahifa.keyingi = kursor ?? null;
      if (kursor !== undefined) {
        keyingi = keyingiHavola(req, { cursor: kursor });
      }
    }
    if (natija.jami !== undefined) sahifa.jami = natija.jami;
    // Link sarlavhasi (RFC 8288) — mijoz URL'ni o'zi yig'masin
    if (keyingi !== undefined) res.links({ next: keyingi });
    // ro'yxat ETag'i — Express'niki (tana xeshi, W/"…"): Nest javobni
    // res.json() bilan yuboradi → If-None-Match mos kelsa 304
    keshYoq(res);
    return {
      versiya: natija.versiya,
      vazifalar: natija.vazifalar.map(
        (v) => proyeksiya(v, maydonlar),
      ),
      sahifa,
    };
  }

  @Post()
  async yarat(
    @Body({ schema: YangiVazifaSxemasi }) body: YangiVazifa,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<Vazifa> {
    const v = await this.xizmat.yarat(body);
    vazifaSarlavhalari(res, v);
    res.location(`${asosYol(req, "vazifalar")}/${String(v.id)}`);
    return v; // POST — sukut 201
  }

  /** PUT to'plamga — butun ro'yxatni almashtirish; atomik */
  @Put()
  async almashtirHammasi(
    @Body({ schema: ToplamSxemasi })
    body: { vazifalar: AlmashtirishVazifasi[] },
    @Res({ passthrough: true }) res: Response,
  ): Promise<object> {
    const { vazifalar, versiya } =
      await this.xizmat.almashtirHammasi(body.vazifalar);
    keshYoq(res);
    return { versiya, vazifalar };
  }

  // Express metod mos kelmasa 404 beradi — 405 + Allow ni o'zimiz
  // qo'yamiz (Allow ro'yxatini marshrutlar.test.ts tekshiradi)
  @All()
  toplamMetodYoq(@Req() req: Request): never {
    throw new MetodYoqXatosi(req.method, "GET, HEAD, POST, PUT");
  }

  /** Bulk yaratish — qisman muvaffaqiyat: 207 Multi-Status
   * (/:id dan OLDIN — aks holda "toplam" id deb olinadi) */
  @Post("toplam")
  async toplam(
    @Body({ schema: BulkSxemasi }) body: { vazifalar: unknown[] },
    @Res({ passthrough: true }) res: Response,
  ): Promise<{ natijalar: Natija[] }> {
    // elementlar alohida tekshiriladi (qisman muvaffaqiyat) — pipe
    // emas, sxema bevosita
    const tekshirilgan = body.vazifalar.map(
      (el) => YangiVazifaSxemasi.safeParse(el),
    );
    const yaroqli = tekshirilgan.flatMap(
      (t) => (t.success ? [t.data] : []),
    );
    const yaratilgan = await this.xizmat.toplamYarat(yaroqli);
    let k = 0;
    const natijalar = tekshirilgan.map((t, indeks): Natija => {
      if (!t.success) {
        const xabar = t.error.issues.map((i) => i.message).join("; ");
        const joy = `body.vazifalar[${String(indeks)}]`;
        return { indeks, holat: 422, xato: { joy, xabar } };
      }
      const vazifa = yaratilgan[k++];
      return vazifa === undefined
        ? { indeks, holat: 500 }
        : { indeks, holat: 201, vazifa };
    });
    res.status(natijalar.every((n) => n.holat === 201) ? 201 : 207);
    return { natijalar };
  }

  @All("toplam")
  toplamPostYoq(@Req() req: Request): never {
    throw new MetodYoqXatosi(req.method, "POST");
  }

  @Get(":id")
  async ol(
    @Param({ schema: IdSxemasi }) { id }: Id,
    @Res({ passthrough: true }) res: Response,
  ): Promise<Vazifa> {
    const v = await this.xizmat.ol(id);
    vazifaSarlavhalari(res, v);
    keshYoq(res);
    return v;
  }

  /** PUT — to'liq almashtirish (idempotent: 2 marta = 1 marta) */
  @Put(":id")
  async almashtir(
    @Param({ schema: IdSxemasi }) { id }: Id,
    @Body({ schema: ToliqVazifaSxemasi }) body: ToliqVazifa,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<Vazifa> {
    const v = await this.xizmat.almashtir(id, body, ifMatch(req));
    vazifaSarlavhalari(res, v);
    return v;
  }

  /** PATCH — qisman (JSON Merge Patch). If-Match: "3" — faqat
   * 3-versiya bo'lsa yoz */
  @Patch(":id")
  async yangila(
    @Param({ schema: IdSxemasi }) { id }: Id,
    @Body({ schema: OzgarishSxemasi }) body: VazifaOzgarishi,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<Vazifa> {
    const v = await this.xizmat.yangila(id, body, ifMatch(req));
    vazifaSarlavhalari(res, v);
    return v;
  }

  @Delete(":id")
  @HttpCode(204)
  async ochir(
    @Param({ schema: IdSxemasi }) { id }: Id,
    @Req() req: Request,
  ): Promise<void> {
    await this.xizmat.ochir(id, ifMatch(req));
  }

  @All(":id")
  birMetodYoq(@Req() req: Request): never {
    throw new MetodYoqXatosi(
      req.method, "GET, HEAD, PUT, PATCH, DELETE",
    );
  }
}

Controller'lar darsidagi holat bilan farqlar:

  • Kirish — parametrda. tekshir(IdSxemasi, req.params, "params") o'rniga @Param({ schema: IdSxemasi }) { id }: Id. Destrukturlash ishlaydi, chunki pipe sxemaning chiqishini beradi: { id: 7 } (son).
  • @Req() kamaydi. ol, almashtirHammasi, toplam endi req siz. Qolgan joylarda u asosYol va ifMatch uchun — keyingi darslarda o'z dekoratorlarimiz bilan almashadi.
  • toplam — tashqi qobiq (BulkSxemasi) pipe'da, elementlar esa handler'da safeParse bilan: qisman muvaffaqiyat uchun («Qisman muvaffaqiyat» mashqi).
  • tekshir va jsonTanaKerak importlari — fayldan yo'qoldi.

9.3 Eksport va AppModule

eksport.controller.ts da faqat boshla metodi o'zgaradi (importga Body qo'shiladi, tekshir importi o'chadi, EksportSorovi turi sxema.ts dan):

ts
@Post()
@HttpCode(202)
boshla(
  @Body({ schema: EksportSxemasi }) body: EksportSorovi,
  @Req() req: Request,
  @Res({ passthrough: true }) res: Response,
): Record<string, unknown> {
  const ish = this.eksport.boshla(body);
  const asos = asosYol(req, "eksportlar");
  res.location(`${asos}/${ish.id}`).set("Retry-After", "1");
  return this.eksport.korinish(ish, asos);
}

app.module.ts da forRoot qaytaradigan dinamik modulga providers qo'shiladi:

ts
import { APP_PIPE } from "@nestjs/core";
import { SxemaPipe } from "./umumiy/sxema.pipe.ts";

// forRoot(...) ichida, return { module, imports, … } ga:
providers: [
  { provide: APP_PIPE, useClass: SxemaPipe },
],

APP_PIPE — "bu provider global pipe" degan maxsus token. app.useGlobalPipes(new SxemaPipe()) ham ishlardi; nega APP_PIPE afzal — Global enhancer'lar va bajarilish tartibi darsida.

9.4 Tekshiruv: ikkita kutilgan xato

npm run tip va npm run lint toza o'tishi kerak. Testlar esa — yo'q. Biz shu holatni kanon nusxasida yurgizdik (biz ishga tushirdik, Vitest 5.0.5; Postgres'siz — bazaga ulanadigan testlar SKIP):

text
 ❯ test/xato.test.ts (3 tests | 1 failed) 1871ms
   × Problem Details: 404 marshrut (about:blank), 405 Allow bilan, 415 215ms
 ❯ test/api.test.ts (5 tests | 1 failed) 2222ms
   × xatolar: 400 JSON, 415, 422, 405 + Allow, 404 125ms

 FAIL  test/api.test.ts > xatolar: 400 JSON, 415, 422, 405 + Allow, 404
AssertionError: Expected values to be strictly equal:

422 !== 415

 ❯ test/api.test.ts:39:10
     37|   assert.deepEqual(await xom.json(), { type: "about:blank", title: "Ba…
     38|   const matnli = await fetch(`${asos}/v1/vazifalar`, { method: "POST",…
     39|   assert.equal(matnli.status, 415);
       |          ^

 Test Files  2 failed | 16 passed | 5 skipped (23)
      Tests  2 failed | 73 passed | 6 skipped (81)

(Chiqishni qisqartirdik: ikkinchi xatoning tafsiloti birinchisi bilan bir xil.) Tarjimasi: "kutilgan 415, keldi 422". Test Content-Type: text/plain bilan POST yuboradi va 415 Unsupported Media Type kutadi. Biz esa 422 qaytardik.

Nega? Avval jsonTanaKerak(req) handler'ning birinchi qatori edi va tekshiruvdan oldin ishlardi. Endi tekshiruv — pipe'da, pipe esa handler'dan oldin. text/plain tanani express.json() o'qimaydi, req.body — undefined, sxema uni rad etadi — 422. Handler'gacha navbat yetmaydi.

Request lifecycle dagi tartib: middleware → guard → interceptor → pipe → handler. 415 tekshiruvi pipe'dan oldin bo'lishi kerak — demak guard'ga. Buni keyingi darsning «Loyiha qadami» da qilamiz.

Faqat shu ikki test yiqilsa — hammasi to'g'ri. Boshqa xato chiqsa (masalan, ParametrXatosi o'rniga 500), uni hozir tuzating. Hali commit qilmang.

10. Real ishda

  • NestJS 12 bilan yangi loyihalarda Zod (yoki Valibot) — { schema } orqali tabiiy tanlov; class-validator — mavjud kod bazalarida va Swagger plagini kerak bo'lganda.
  • Paket tanlashda npm view <paket> peerDependencies — "bu Nest versiyamni qo'llaydimi" degan savolning eng tez javobi.
  • Intervyu: "Standard Schema nima?", "class-validator va Zod farqi?", "validatsiya qaysi qatlamda — pipe, guard yoki xizmatda?".

Xulosa

  • NestJS 12: @Body/@Query/@Param({ schema }) — sxemani parametr metama'lumotiga yozadi; pipe metadata.schema dan oladi.
  • Standard Schema — ~standard.validate(): Zod, Valibot, ArkType bir xil ulanadi; handler sxema chiqishini oladi.
  • StandardSchemaValidationPipe — tayyor, lekin bitta status; kanon — o'z SxemaPipe (422/400).
  • nestjs-zod 5.5.0 peer bog'liqligi Nest 12 ni qo'llamaydi (2026-11-02): @nestjs/common va @nestjs/swagger — 11 gacha; o'rnatilgan yo'l uni ortiqcha qiladi. Paket tanlashdan oldin — npm view <paket> peerDependencies.
  • Kanon o'lchovi: ro'yxat so'rovida Zod ~2.2× tez, bitta manba, yangi bog'liqliksiz — Zod qoldi; class-validator — o'lchab asoslangan muqobil.

Keyingi dars: Javobni serializatsiya qilish — kirishni tekshirdik, endi chiqish: parolHash kabi maydon javobga tushmasligi. Loyiha qadamida esa 415 ni guard bilan qaytaramiz va commit qilamiz.

Manbalar

  • NestJS hujjati — Pipes (schema-based validation, Standard Schema): https://docs.nestjs.com/pipes
  • Standard Schema spetsifikatsiyasi: https://standardschema.dev
  • Zod 4 — strictObject, coerce, lokalizatsiya: https://zod.dev
  • nestjs-zod — npm: https://www.npmjs.com/package/nestjs-zod va paket fayli https://unpkg.com/nestjs-zod@5.5.0/package.json (peerDependencies, ikkala manbada bir xil — 2026-11-02 holatida)
  • Kanon (IlmHamroh, 2026-10-27): Zod va class-validator variantlari — HTTP yuk o'lchovi (autocannon, mediana), bitta tana tekshiruvi (200 000 aylana), 33 so'rovli solishtiruv, bog'liqliklar. Demo (StandardSchemaValidationPipe, nestjs-zod va npm xatosi), oraliq holatdagi testlar — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, zod 4.6.7, nestjs-zod 5.5.0, Node 24.21.0).
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Zod va Standard Schema NestJS'da: @Body({ schema }), nestjs-zod va o'lchov — IlmHamroh