IlmHamroh
JavaScript Full-stack/30-qism. NestJS20/65-dars19 daqiqa
Mundarija (32)

Javobni serializatsiya qilish NestJS'da: ClassSerializerInterceptor, @Exclude va plain obyekt tuzog'i

Qisqacha: Kirishni tekshirish yetarli emas — chiqish ham nazorat qilinadi: parolHash kabi maydon javobga tushmasligi kerak. Nest standarti — ClassSerializerInterceptor + class-transformer dekoratorlari: @Exclude() — maydon chiqmaydi, @Expose({ groups: ["admin"] }) — faqat admin'ga, @Transform — qiymatni o'zgartiradi. Lekin bu faqat handler klass nusxasini qaytarganda ishlaydi. ORM yoki SQL natijasi kabi oddiy obyekt qaytsa — @Exclude jim e'tiborsiz qoladi va parolHash javobga chiqadi. Ishonchliroq yo'l — javob sxemasi (ruxsat ro'yxati): faqat sanalgan maydonlar chiqadi.

Bu darsda

  • Javob serializatsiyasi nima uchun alohida qatlam ekanini tushuntira olasiz.
  • ClassSerializerInterceptor, @Exclude, @Expose, @Transform, guruhlar va @SerializeOptions ni ishlatasiz.
  • Plain obyekt tuzog'ini haqiqiy javobda ko'rasiz va undan himoyalanish yo'llarini bilasiz.
  • Response DTO va Zod javob sxemasini "ruxsat ro'yxati" sifatida solishtirasiz.
  • vazifalar-api da 415 tekshiruvini guard'ga ko'chirib, loyihaning shu bosqichini commit qilasiz.

Oldin bilishingiz kerak: DTO va class-validator, Zod va Standard Schema NestJS'da, Request lifecycle, Filtrlash, saralash va maydon tanlash.

O'tgan besh dars kirish haqida edi: mijoz yuborgan narsani tekshirdik. Endi teskari yo'nalish — server qaytaradigan narsa.

1. Nega bu kerak?

Tasavvur qiling: GET /foydalanuvchilar/1 handler'i bazadan foydalanuvchini o'qiydi va qaytaradi. Bazadagi qatorda id, email, ism — va parol_hash, oxirgi_ip, ochirilgan_vaqt ham bor. Handler return foydalanuvchi qilsa — hammasi JSON'ga aylanadi va mijozga ketadi.

Bu ixtiro qilingan xavf emas. OWASP API Security Top 10 (2023) da uchinchi o'rin — Broken Object Property Level Authorization: API obyektning o'qilishi mumkin bo'lmagan xususiyatlarini qaytaradi. Parol hash'i oshkor bo'lsa, uni oflayn buzishga urinishadi (crypto: parolni hash'lash).

Kirish tomonda whitelist ortiqcha maydonni kiritmaydi. Chiqish tomonda shunga o'xshash qoida kerak: faqat ruxsat etilgan maydonlar chiqsin. Bu jarayon serializatsiya (serialization) deyiladi — server ichidagi obyektni tashqariga yuboriladigan shaklga (JSON) aylantirish.

1.1 Express'da qanday edi

25–27-qismlarda biz bu muammoni ikki joyda hal qilgandik. Birinchisi — ombor: SQL so'rovi faqat kerakli ustunlarni tanlaydi va natijani Vazifa turiga o'tkazadi. Ichki ustunlar (egasi_id, ochirilgan_vaqt) handler'gacha yetib kelmaydi. Ikkinchisi — ?fields= va proyeksiya(): mijoz so'ragan maydonlar faqat ruxsat ro'yxatidan tanlanadi.

Ikkalasi ham ruxsat ro'yxati: "nima chiqsin" aniq yozilgan. Nest'ning standart yo'li esa boshqacha — klass dekoratorlari bilan "nima chiqmasin" deyiladi. Bu dars ikkalasini solishtiradi va qachon qaysi biri xavfsizroq ekanini ko'rsatadi.

O'xshatish: kasalxona arxividan ma'lumotnoma olish. Arxivda bemorning to'liq kartasi bor — tashxis, tahlillar, uy manzili. Ma'lumotnomaga esa faqat kerakli qatorlar ko'chiriladi. Ko'chiruvchi "nima yozilmasin" ro'yxatiga emas, "nima yozilsin" ro'yxatiga qaraydi — yangi maxfiy ustun qo'shilsa ham u ma'lumotnomaga tushmaydi.

2. ClassSerializerInterceptor

2.1 G'oya

Nest'da standart yo'l — interceptor. Interceptor handler'ni ikki tomondan o'raydi: undan oldin va undan keyin ishlaydi (Request lifecycle dagi tartibda — pipe'lardan oldin kiradi, handler'dan keyin chiqadi). Interceptor'larni to'liq Interceptor'lar va RxJS darsida o'rganamiz. Hozir bitta tayyorini ishlatamiz.

ClassSerializerInterceptor handler qaytargan qiymatni oladi va class-transformer'ning instanceToPlain funksiyasidan o'tkazadi. Bu funksiya klass dekoratorlarini o'qiydi: @Exclude li maydonni tashlaydi, @Expose va @Transform ni qo'llaydi. Natija — oddiy obyekt, Nest uni JSON qilib yuboradi.

flowchart LR
  H["handler<br/>return nusxa"] --> I["ClassSerializer<br/>Interceptor"]
  I --> T["instanceToPlain:<br/>@Exclude, @Expose,<br/>@Transform"]
  T --> J["JSON javob"]

Diagrammadagi asosiy narsa — instanceToPlain ning nomi: instance — klass nusxasi. Qoidalar nusxaning klassidan o'qiladi. Shu nomda darsning asosiy tuzog'i yashirin.

2.2 Javob klassi

Kanon tajribasi (kurs tajribasi, NestJS 12.2.0, natijalar Vitest ichida) foydalanuvchi javobi uchun klass yozgan. Bazadagi qator va klass:

ts
class FoydalanuvchiJavobi {
  id!: number;
  email!: string;
  @Exclude() parolHash!: string;
  @Expose({ groups: ["admin"] }) oxirgiKirish!: string;
  @Transform(({ value }: { value: string }) => value.toUpperCase())
  ism!: string;
  @Expose() get qisqaEmail(): string {
    return this.email.split("@")[0] ?? "";
  }
  constructor(q: Partial<FoydalanuvchiJavobi>) {
    Object.assign(this, q);
  }
}

const bazadan = {
  id: 1, email: "aziz@example.com",
  parolHash: "$argon2id$v=19$…",
  oxirgiKirish: "2026-10-27T10:00:00Z", ism: "aziz",
};
  • @Exclude() — maydon javobga chiqmaydi.
  • @Expose({ groups: ["admin"] }) — faqat admin guruhi so'ralganda chiqadi.
  • @Transform(fn) — chiqishda qiymat o'zgaradi (bu yerda — katta harf).
  • @Expose() get qisqaEmail() — getter (Getter/setter va static a'zolar) odatda serializatsiyaga kirmaydi; @Expose uni qo'shadi.

2.3 Controller va natija

ts
@Controller()
@UseInterceptors(ClassSerializerInterceptor)
class C {
  @Get("klass") klass() { return new FoydalanuvchiJavobi(bazadan); }
  @Get("plain") plain() { return bazadan; }
  @Get("admin") @SerializeOptions({ groups: ["admin"] })
  admin() { return new FoydalanuvchiJavobi(bazadan); }
  @Get("type") @SerializeOptions({ type: FoydalanuvchiJavobi })
  tipBilan() { return bazadan; }
}

@UseInterceptors(ClassSerializerInterceptor) — interceptor shu controller'ning hamma marshrutiga. To'rt marshrut bir xil foydalanuvchini qaytaradi, faqat turli yo'l bilan. Har javobda parolHash bor-yo'qligiga va ism qanday yozilganiga qarang:

text
GET /klass — klass nusxasi
  {"id":1,"email":"aziz@example.com","ism":"AZIZ","qisqaEmail":"aziz"}
GET /plain — plain obyekt (TUZOQ)
  {"id":1,"email":"aziz@example.com","parolHash":"$argon2id$v=19$…","oxirgiKirish":"2026-10-27T10:00:00Z","ism":"aziz"}
GET /admin — groups: admin
  {"id":1,"email":"aziz@example.com","oxirgiKirish":"2026-10-27T10:00:00Z","ism":"AZIZ","qisqaEmail":"aziz"}
GET /type — @SerializeOptions({ type }) — plain obyektni klassga aylantiradi
  {"id":1,"email":"aziz@example.com","ism":"AZIZ","qisqaEmail":"aziz"}
  • /klass — hammasi kutilganidek: parolHash yo'q, oxirgiKirish yo'q (guruh so'ralmagan), ism — katta harf, qisqaEmail qo'shildi.
  • /plain — parolHash javobda. ism ham o'zgarmagan, qisqaEmail yo'q. Ya'ni hech qaysi dekorator ishlamadi.
  • /admin — @SerializeOptions({ groups: ["admin"] }) bilan oxirgiKirish chiqdi.
  • /type — handler yana oddiy obyekt qaytardi, lekin @SerializeOptions({ type }) interceptor'ga "buni avval shu klassga aylantir" dedi. Natija /klass bilan bir xil.

2.4 Global ulash

Har controller'ga @UseInterceptors yozish — unutish uchun yana bir joy. Shuning uchun ko'p loyihada interceptor global ulanadi. Nest hujjatidagi shakl:

ts
app.useGlobalInterceptors(
  new ClassSerializerInterceptor(app.get(Reflector)),
);

Reflector — interceptor @SerializeOptions belgilarini o'qishi uchun (Reflector, metadata va DiscoveryService darsidagi o'sha vosita). new bilan yaratganimiz uchun uni app.get(Reflector) bilan o'zimiz beramiz. Muqobil — modulda { provide: APP_INTERCEPTOR, useClass: ClassSerializerInterceptor }: bunda Reflector ni DI beradi. Ikki usul farqi — Global enhancer'lar va bajarilish tartibi darsida.

Global ulash xavfsizlikni oshiradi va yangi controller'larni ham qamraydi, lekin keyingi bo'limdagi tuzoqni yo'qotmaydi: interceptor qayerda ulangan bo'lmasin, oddiy obyektda dekoratorlar baribir ishlamaydi.

3. Plain obyekt tuzog'i

3.1 Nega /plain buzildi

bazadan — oddiy obyekt: uning klassi Object. instanceToPlain qoidalarni obyektning klassidan qidiradi; Object da hech qanday @Exclude yo'q — demak hammasi chiqadi. Xato ham, ogohlantirish ham yo'q.

Buni kichik modelda ko'raylik — qoidalar klass bo'yicha Map da saqlanadi (Map):

js
const QOIDALAR = new Map();
function exclude(klass, maydon) {
  const eski = QOIDALAR.get(klass) ?? [];
  QOIDALAR.set(klass, [...eski, maydon]);
}
function instanceToPlain(obyekt) {
  const yashir = QOIDALAR.get(obyekt.constructor) ?? [];
  return Object.fromEntries(
    Object.entries(obyekt).filter(([k]) => !yashir.includes(k)),
  );
}

class FoydalanuvchiJavobi {
  constructor(q) { Object.assign(this, q); }
}
exclude(FoydalanuvchiJavobi, "parolHash");

const bazadan = { id: 1, email: "aziz@example.com", parolHash: "x" };
console.log(instanceToPlain(new FoydalanuvchiJavobi(bazadan)));
console.log(instanceToPlain(bazadan));
console.log(bazadan.constructor.name);

Konsolda:

text
{ id: 1, email: 'aziz@example.com' }
{ id: 1, email: 'aziz@example.com', parolHash: 'x' }
Object

Bir xil ma'lumot, ikki xil natija — farqi faqat qaysi klassning nusxasi ekanida. Birinchi qatorda qoida topildi va maxfiy maydon tashlandi; ikkinchisida Map dan Object uchun hech narsa chiqmadi va hammasi o'tdi. Model soddalashtirilgan: class-transformer qoidalarni o'z metama'lumot omborida saqlaydi va meros zanjirini ham ko'radi. Lekin "qoida klassga bog'liq, oddiy obyektda — yo'q" degan asos aynan shunday.

3.2 Real hayotda plain obyekt qayerdan keladi

Deyarli hamma joydan:

  • Prisma va Drizzle natijalari — oddiy obyektlar (Prisma 7).
  • pg drayverining rows — oddiy obyektlar.
  • Redis keshidan JSON.parse qilingan qiymat — oddiy obyekt. Keshli va keshsiz yo'l turlicha javob berishi mumkin!
  • { ...foydalanuvchi, rol } — spread klass nusxasidan ham oddiy obyekt yasaydi.

Oxirgisi eng xavflisi: bitta "kichik qulaylik" (return { ...f, rol }) va parolHash chiqib ketdi. Test faqat "javobda email bor" ni tekshirsa — buni sezmaydi.

3.3 Himoya yo'llari

Yo'l Qanday Kamchiligi
@SerializeOptions({ type }) interceptor plain'ni klassga aylantiradi har marshrutga yozish kerak; unutilsa — yana oqish
excludeExtraneousValues: true faqat @Expose qilingan maydonlar chiqadi hamma maydonga @Expose kerak
Javob sxemasi (Zod) JavobSxemasi.parse(qiymat) — faqat sanalganlar sxemani alohida yozish
Ombordan to'g'ri tanlash SELECT id, email, ism, Prisma select qatlamlar orasida kelishuv

Birinchi ustunning ikkinchi va uchinchi qatorlari — ruxsat ro'yxati (allowlist): yangi maxfiy maydon qo'shilsa, u avtomatik chiqmaydi. @Exclude esa taqiq ro'yxati (denylist): har yangi maxfiy maydonni eslab qo'shish kerak. Xavfsizlikda ruxsat ro'yxati afzal (Validatsiya: nima uchun, qayerda va qanday).

Tekshirib ko'ring: FoydalanuvchiJavobi da parolHash @Exclude() bilan belgilangan va interceptor ulangan. Handler: const f = await this.ombor.top(id); return { ...f, toliqIsm: f.ism + " " + f.familiya };. ombor.top klass nusxasini qaytaradi. parolHash chiqadimi?

Javob

Chiqadi. Spread { ...f } yangi oddiy obyekt yasaydi — klassi Object. @Exclude qoidasi FoydalanuvchiJavobi klassida qoldi, yangi obyektda u yo'q. Yechim: toliqIsm ni klassga getter (@Expose() get toliqIsm()) qilib qo'shish, yoki @SerializeOptions({ type: FoydalanuvchiJavobi }), yoki javob sxemasi.

4. Javob sxemasi va response DTO

4.1 Zod bilan

Kanon tajribasida beshinchi marshrut:

ts
const JavobSxemasi = z.object({
  id: z.number(), email: z.string(), ism: z.string(),
});

@Get("zod") zod() { return JavobSxemasi.parse(bazadan); }
text
GET /zod — Zod javob sxemasi (strip)
  {"id":1,"email":"aziz@example.com","ism":"aziz"}

z.object sxemada yo'q kalitlarni jim olib tashlaydi (strip). Kirishda bu xulq bizga yoqmagan edi — u yerda z.strictObject ishlatamiz, chunki noma'lum maydon mijoz xatosi. Chiqishda esa aynan shu kerak: bazada nima bo'lmasin, javobga faqat id, email, ism chiqadi. Plain obyekt ham, klass nusxasi ham — farqi yo'q.

4.2 Response DTO

Nest hujjatlarida ko'p uchraydigan yana bir naqsh — alohida javob DTO'si: class FoydalanuvchiJavobDto { id; email; ism } va xizmat bazadagi yozuvni shu klassga o'tkazib qaytaradi (new FoydalanuvchiJavobDto(...) yoki plainToInstance). Bu ham ruxsat ro'yxati, faqat klass bilan. Swagger javob turini shu klassdan oladi (Swagger).

Qaysi birini tanlash — loyihaning kirish tomoniga qarab: kirish class-validator'da bo'lsa — javob DTO'lari ham klass; kirish Zod'da bo'lsa — javob sxemasi ham Zod.

4.3 Qaysi yo'lni tanlash

Uch savol:

  1. Javobda maxfiy maydon bo'lishi mumkinmi? Yo'q bo'lsa (vazifa, teg) — ombordan to'g'ri ustunlarni tanlash yetarli. Bor bo'lsa (foydalanuvchi, sessiya, to'lov) — qo'shimcha himoya kerak.
  2. Kirish qaysi kutubxonada? Zod bo'lsa — javob sxemasi ham Zod; class-validator bo'lsa — javob DTO'si va ClassSerializerInterceptor. Bitta loyihada ikki uslub aralashsa, o'qish qiyinlashadi.
  3. Ruxsat ro'yxatimi yoki taqiq ro'yxati? Maxfiy maydonli javobda — doim ruxsat ro'yxati (excludeExtraneousValues, javob sxemasi yoki javob DTO'si). @Exclude — qo'shimcha qatlam sifatida yaxshi, yagona himoya sifatida emas.

Tekshirib ko'ring: GET /foydalanuvchilar/1 javobi 60 soniya Redis'da keshlanadi. Birinchi so'rovda handler klass nusxasini qaytaradi va @Exclude ishlaydi. Kesh interceptor'i esa javobni serializatsiyadan oldin keshga yozadi. Ikkinchi so'rovda nima chiqadi?

Javob

Keshdan JSON.parse qilingan oddiy obyekt keladi. Agar keshga yozilgan qiymatda parolHash bo'lsa (u klass nusxasining maydoni sifatida saqlangan), ikkinchi javobda u chiqadi. Birinchi so'rov xavfsiz, keyingi 60 soniya — yo'q. Yechim: keshga allaqachon serializatsiya qilingan javobni yozing yoki javob sxemasini keshdan keyin qo'llang. Interceptor'lar tartibi — Global enhancer'lar va bajarilish tartibi darsida.

4.4 vazifalar-api da

Vazifa da maxfiy maydon yo'q: id, matn, bajarildi, teglar, yaratildi, yangilandi, tartib, versiya — hammasi mijozga tegishli. Ichki maydonlar (egasi_id, ochirilgan_vaqt — 26–27-qismlar sxemasidan) ombor qatlamida Vazifa turiga o'tkazilmaydi: ombor faqat kerakli ustunlarni tanlaydi. Bu — jadvaldagi to'rtinchi yo'l.

Bundan tashqari, 25-qismdan ?fields=id,matn bor — mijoz maydonlarni o'zi tanlaydi va proyeksiya() faqat ruxsat etilgan ro'yxatdan (MAYDONLAR) qoldiradi (Filtrlash, saralash va maydon tanlash). Shuning uchun kanonda ClassSerializerInterceptor yo'q. U foydalanuvchi va sessiya javoblarida kerak bo'ladi — auth Nest'ga ko'chganda.

5. Hujumchi nigohi

  1. Maxfiy maydonni ovlash. Hujumchi har endpoint javobini o'qiydi va kutilmagan maydonlarni qidiradi: parolHash, resetToken, totpSecret, ichki_izoh. Bitta oqish — butun hisobga yo'l. Himoya — ruxsat ro'yxati (javob sxemasi, excludeExtraneousValues, SELECT da aniq ustunlar).
  2. Kesh orqali oqish. Birinchi so'rov klass nusxasini qaytaradi (@Exclude ishlaydi), keyingisi Redis keshidan oddiy obyektni — maxfiy maydon bilan. Himoya — keshga allaqachon serializatsiya qilingan javobni yozish yoki sxemani keshdan keyin qo'llash.
  3. Guruhni almashtirish. ?guruh=admin kabi parametr bilan serializatsiya guruhini mijoz tanlasa — admin maydonlari ochiladi. Himoya — guruh faqat serverda, foydalanuvchi roliga qarab aniqlanadi.
  4. Xato javobida oqish. 500 javobiga xato obyekti yoki stack tushsa — ichki yo'llar, SQL so'rovi ko'rinadi. Himoya — Problem Details filtri faqat umumiy detail qaytaradi, tafsilot — logga (Exception filter'lar).

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
@Exclude li maydon javobda handler oddiy obyekt qaytardi @SerializeOptions({ type }) yoki javob sxemasi
{ ...nusxa } dan keyin maydon chiqdi spread klassni yo'qotadi getter + @Expose yoki sxema
Getter javobda yo'q @Expose() qo'yilmagan @Expose() get x()
@Exclude umuman ishlamaydi interceptor ulanmagan @UseInterceptors(ClassSerializerInterceptor) yoki global
Kesh bilan va keshsiz javob farqli keshdan plain obyekt serializatsiyadan keyin keshlash

7. Mashqlar

Mashqlar kurs/mashqlar/30/20-serializatsiya/ da.

1-mashq (oson): dekoratorni tanlang

  • Maydon javobga chiqmasin — @()
  • Getter javobga chiqsin — @() get qisqaEmail()
  • Faqat admin ko'rsin — @Expose({ : ["admin"] })
  • Oddiy obyektni klassga aylantirib serializatsiya — @SerializeOptions({ : FoydalanuvchiJavobi })
  • Javob uchun Zod'da noma'lum kalitni jim tashlaydigan — z.
Yechim
  • Exclude — taqiq ro'yxati: faqat klass nusxasida ishlaydi.
  • Expose — getter va guruhli maydonlar uchun; excludeExtraneousValues bilan — ruxsat ro'yxati.
  • groups — guruh @SerializeOptions({ groups }) bilan yoqiladi.
  • type — plain obyekt tuzog'idan himoya.
  • object — chiqishda strip foydali; kirishda esa strictObject.

2-mashq (o'rta): javob sxemasi modeli

javob(obyekt, sxema) funksiyasini yozing. sxema — { maydon: tur } ("son" yoki "satr"). Natija — faqat sxemadagi maydonlar; tur mos kelmasa — xato tashlansin (javob sxemasi buzilishi — server xatosi). Obyektda sxemada yo'q maydonlar qancha bo'lmasin — chiqmasin. Ishora: Object.entries, typeof.

Yechim
js
function javob(obyekt, sxema) {
  const natija = {};
  for (const [m, tur] of Object.entries(sxema)) {
    const v = obyekt[m];
    const kutilgan = tur === "son" ? "number" : "string";
    if (typeof v !== kutilgan) {
      throw new Error(`javob sxemasi: ${m} — ${tur} emas`);
    }
    natija[m] = v;
  }
  return natija;
}

const sxema = { id: "son", email: "satr", ism: "satr" };
const bazadan = {
  id: 1, email: "aziz@example.com", ism: "Aziz",
  parolHash: "$argon2id$…", totpSecret: "JBSWY3DP",
};
console.log(javob(bazadan, sxema));
console.log(javob({ ...bazadan, rol: "admin" }, sxema));
try {
  javob({ id: "1", email: "a@b.uz", ism: "A" }, sxema);
} catch (x) {
  console.log(x.message);
}

Konsolda:

text
{ id: 1, email: 'aziz@example.com', ism: 'Aziz' }
{ id: 1, email: 'aziz@example.com', ism: 'Aziz' }
javob sxemasi: id — son emas

Ikkinchi qator — spread ham, yangi rol maydoni ham natijaga ta'sir qilmadi. Ruxsat ro'yxatining kuchi shu: bazaga ertaga resetToken ustuni qo'shilsa ham, javob o'zgarmaydi. Uchinchi qator — tur buzilsa, xato mijozga noto'g'ri ma'lumot yuborishdan yaxshiroq (bu 500, chunki server o'z shartnomasini buzdi).

3-mashq (qiyin): guruhlar modeli

Klass qoidalarini oddiy obyekt sifatida bering: { parolHash: { exclude: true }, oxirgiKirish: { groups: ["admin"] }, ism: { transform: (v) => v.toUpperCase() } }. serializatsiya(obyekt, qoidalar, guruhlar) funksiyasi: exclude — chiqmaydi; groups bor bo'lsa — faqat so'ralgan guruhlardan biri mos kelsa chiqadi; transform — qo'llanadi; qoidasiz maydon — o'zgarishsiz. Ishora: some (Massivda qidirish va tekshirish).

Yechim
js
function serializatsiya(obyekt, qoidalar, guruhlar = []) {
  const natija = {};
  for (const [m, v] of Object.entries(obyekt)) {
    const q = qoidalar[m] ?? {};
    if (q.exclude) continue;
    if (q.groups && !q.groups.some((g) => guruhlar.includes(g))) {
      continue;
    }
    natija[m] = q.transform ? q.transform(v) : v;
  }
  return natija;
}

const qoidalar = {
  parolHash: { exclude: true },
  oxirgiKirish: { groups: ["admin"] },
  ism: { transform: (v) => v.toUpperCase() },
};
const f = {
  id: 1, ism: "aziz", parolHash: "x", oxirgiKirish: "2026-10-27",
};
console.log(serializatsiya(f, qoidalar));
console.log(serializatsiya(f, qoidalar, ["admin"]));

Konsolda:

text
{ id: 1, ism: 'AZIZ' }
{ id: 1, ism: 'AZIZ', oxirgiKirish: '2026-10-27' }

Natijalar kanon tajribasidagi /klass va /admin ga mos (getter'siz). Bu model — taqiq ro'yxati: qoidasiz maydon chiqadi. Uni ruxsat ro'yxatiga aylantirish uchun bitta qator yetadi: if (!(m in qoidalar)) continue; — shunda faqat qoidasi bor maydonlar chiqadi (excludeExtraneousValues ning modeli).

8. Loyiha qadami: 415 guard'da va commit

Javob tomonida vazifalar-api ga o'zgarish kerak emas (yuqoridagi «vazifalar-api da» bo'limi). Bu qadamda o'tgan darsda qizil qolgan ikki testni tuzatamiz va loyihaning shu bosqichini yopamiz.

8.1 Muammo

O'tgan darsda tekshiruv pipe'ga ko'chdi va text/plain POST 415 o'rniga 422 oldi: pipe handler'dan oldin ishlaydi, jsonTanaKerak(req) esa handler ichida edi. 415 tekshiruvi pipe'dan oldin bo'lishi kerak. Request lifecycle da pipe'dan oldin — guard'lar va interceptor'lar.

Guard (himoyachi) — "bu so'rov handler'ga o'tishi mumkinmi?" savoliga javob beradigan klass. U canActivate() metodini bajaradi. Guard'larni to'liq Guard'lar darsida o'rganamiz; hozir bitta oddiy guard yetadi.

Lekin hamma marshrut JSON tana talab qilmaydi: GET va DELETE da tana yo'q. Guard qaysi handler'da tekshirishni qayerdan biladi? Reflector, metadata va DiscoveryService darsidagi usul bilan: handler'ga metama'lumot belgisi qo'yiladi, guard uni Reflector bilan o'qiydi.

8.2 src/umumiy/json-tana.guard.ts

Kanon faylida qatorlar uzun edi — telefon uchun bo'ldik va izohlarni qisqartirdik (kod ma'nosi aynan); nusxani kanon nusxasida tsc --noEmit, ESLint va Vitest bilan tekshirdik.

ts
// json-tana.guard.ts — tanali so'rov faqat JSON bo'lsin (415).
// Express'da route middleware edi (jsonKerak); Nest'da route
// darajasidagi middleware yo'q, pipe esa guard'dan KEYIN ishlaydi —
// 415 validatsiyadan (422) OLDIN chiqishi uchun guard +
// metama'lumot dekoratori. @JsonTana() belgisiz marshrut (GET,
// DELETE, @All 405) tekshirilmaydi
import { Injectable, SetMetadata } from "@nestjs/common";
import type { CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import type { Request } from "express";
import { jsonTanaKerak } from "./json-kerak.ts";

const JSON_TANA = "vazifalar:json-tana";
/** Handler'ga: "tana JSON bo'lishi shart". Reflector.createDecorator
 * emas: uning get() turi undefined'ni hisobga olmaydi — lint "shart
 * doim true" deydi. SetMetadata + get<T | undefined> — aniq */
export const JsonTana = (): MethodDecorator =>
  SetMetadata(JSON_TANA, true);

@Injectable()
export class JsonTanaGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(kontekst: ExecutionContext): boolean {
    const kerak = this.reflector.get<boolean | undefined>(
      JSON_TANA, kontekst.getHandler(),
    );
    if (kerak === true) {
      jsonTanaKerak(kontekst.switchToHttp().getRequest<Request>());
    }
    // false qaytarsak Nest 403 beradi — bizga 415 kerak: shuning
    // uchun jsonTanaKerak o'zi IlovaXatosi(415) tashlaydi
    return true;
  }
}
  • JsonTana() — SetMetadata bilan belgi qo'yadigan dekorator. U 25-qismdagi jsonKerak route middleware'ining o'rni: "shu marshrutga JSON tana kerak".
  • canActivate — belgi bo'lsa, o'sha jsonTanaKerak(req) ni chaqiradi (fayl o'zgarmaydi). Xato tashlansa — so'rov filtrga ketadi va 415 Problem Details oladi.
  • return true — guard false qaytarsa, Nest o'zi 403 Forbidden beradi. Bizga boshqa status kerak — shuning uchun rad etish xato tashlash orqali.
  • Reflector.createDecorator emas — sabab izohda; haqiqiy lint chiqishi bilan Guard'lar darsida ko'ramiz.

8.3 Ulash

vazifalar.controller.ts da import va beshta belgi qo'shiladi — tanali marshrutlarga:

diff
+import { JsonTana } from "../umumiy/json-tana.guard.ts";
 …
   @Post()
+  @JsonTana()
   async yarat(
 …
   @Put()
+  @JsonTana()
   async almashtirHammasi(
 …
   @Post("toplam")
+  @JsonTana()
   async toplam(
 …
   @Put(":id")
+  @JsonTana()
   async almashtir(
 …
   @Patch(":id")
+  @JsonTana()
   async yangila(

app.module.ts da guard global qilinadi — APP_GUARD tokeni bilan, APP_PIPE yoniga:

ts
import { APP_GUARD, APP_PIPE } from "@nestjs/core";
import { SxemaPipe } from "./umumiy/sxema.pipe.ts";
import { JsonTanaGuard } from "./umumiy/json-tana.guard.ts";

// forRoot(...) ichida:
// global enhancer'lar DI bilan: Standard Schema tekshiruvi va 415
// guard'i — hamma controller'ga
providers: [
  { provide: APP_PIPE, useClass: SxemaPipe },
  { provide: APP_GUARD, useClass: JsonTanaGuard },
],

JsonTanaGuard constructor'ida Reflector bor — uni Nest DI orqali beradi. APP_GUARD shuni ta'minlaydi; new JsonTanaGuard(...) ni qo'lda yaratish shart emas.

eksport.controller.ts ning boshla metodiga @JsonTana() qo'yilmaydi — 25-qismda ham u yerda jsonKerak yo'q edi: tanasiz POST /v1/eksportlar ruxsat etilgan (sukut — CSV).

8.4 Tekshiruv

Kanon shu holatni to'liq yurgizgan (kanon o'lchovi, IlmHamroh, 2026-10; Postgres 18 bilan, mashina yuklangan):

text
--- npm run tip
exit 0 (11459 ms)
--- npm run lint
exit 0 (31346 ms)
--- npm run test
 ↓ test/konteyner.test.ts (1 test | 1 skipped)
 Test Files  22 passed | 1 skipped (23)
      Tests  117 passed | 1 skipped (118)
exit 0 (13182 ms)
--- npm run build
exit 0 (14867 ms)

O'tgan darsdagi ikki test (api.test, xato.test) yashil. konteyner.test — Testcontainers testi, alohida yoqiladi (sukut SKIP). Postgres'siz ishlatsangiz, bazaga ulanadigan testlar ham SKIP bo'ladi — biz shu holatda yurgizdik: Tests 75 passed | 6 skipped (81).

Jonli tekshiruv: kanon ilovani dist/main.js dan uch saqlash rejimida ishga tushirib, 9 ta so'rov yuborgan — status, sarlavhalar va tanalar oldingi holat bilan bir xil. Masalan, yaratish:

text
$ curl -X POST -H Content-Type: application/json -d {"matn":"Postgres repository yozish #o'qish"} /v1/vazifalar
HTTP/1.1 201 Created
ETag: "1"
Last-Modified: Tue, 27 Oct 2026 15:17:35 GMT
Location: /v1/vazifalar/49
Content-Type: application/json; charset=utf-8
Content-Length: 194

(Manzildagi http://127.0.0.1:<port> ni olib tashladik.)

8.5 Commit

package.json da yangi dev bog'liqlik bor (@standard-schema/spec 1.1.0, o'tgan darsdan). Commit xabari uzun — uni ikki qismda yig'amiz:

bash
git add -A
M="refactor: kirish tekshiruvi Nest 12 Standard Schema bilan"
M+=" (@Body/@Query/@Param({ schema }) + SxemaPipe),"
M+=" 415 — JsonTana guard'i"
git commit -m "$M"

Commit'dan oldin git status bilan tarmoqni tekshiring. Validatsiya darslarida tajriba/class-validator tarmog'iga o'tgan bo'lsangiz, git switch nest bilan qayting — commit nest tarmog'iga tushishi kerak. main 29-qismdagi Express API bo'lib qoladi.

Bu commit bilan vazifalar-api kirishni Nest uslubida tekshiradi: sxema — parametrda, tekshiruv — global pipe'da, 415 — guard'da. Handler'lar ichida tekshir() va jsonTanaKerak() chaqiruvlari qolmadi.

9. Real ishda

  • Foydalanuvchi, to'lov, sessiya javoblari — serializatsiya eng muhim joy. Ko'p jamoalarda qoida: "entity'ni hech qachon to'g'ridan qaytarma — javob DTO yoki sxema".
  • Code review: return user yoki return { ...user } — darhol savol: "parol hash'i chiqmaydimi?".
  • Testlarda faqat "javobda email bor" ni emas, "javobda faqat shu maydonlar bor" ni tekshiring: expect(Object.keys(javob)).toEqual([...]). Shunda yangi maxfiy ustun javobga tushsa — test darhol yiqiladi.
  • Global ClassSerializerInterceptor — ko'p Nest loyihalarida main.ts da yoqilgan. Uni ko'rsangiz, ombor qatlami klass nusxasi qaytarishini tekshiring.
  • Intervyu: "@Exclude qachon ishlamaydi?", "taqiq ro'yxati va ruxsat ro'yxati farqi?", "javobdan maydonni qanday yashirasiz?".

Xulosa

  • Serializatsiya — chiqish nazorati; OWASP API3:2023 — obyekt xususiyatlarining oshkor bo'lishi.
  • ClassSerializerInterceptor + @Exclude/@Expose/@Transform, guruhlar — @SerializeOptions({ groups }).
  • Asosiy tuzoq: oddiy obyekt (ORM natijasi, spread, kesh) — dekoratorlar ishlamaydi, parolHash chiqadi.
  • Himoya — ruxsat ro'yxati: @SerializeOptions({ type }), excludeExtraneousValues, javob sxemasi (Zod z.object strip), SELECT da aniq ustunlar.
  • Global ulash — useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))) yoki APP_INTERCEPTOR; lekin global interceptor ham oddiy obyektni himoya qilmaydi.
  • Keshlangan javob — oddiy obyekt: serializatsiyadan keyin keshlang.
  • vazifalar-api: maxfiy maydon yo'q; 415 — JsonTanaGuard (APP_GUARD) da; loyiha qadami nest tarmog'ida commit qilindi.

Keyingi dars: Middleware Nest'da — 25-qismdagi idempotentlik va eskirgan yo'l middleware'larini Nest modullariga ko'chiramiz va Nest 12.2 ning ikki haqiqiy kamchiligini testlar bilan topamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Javobni serializatsiya qilish NestJS'da: ClassSerializerInterceptor, @Exclude va plain obyekt tuzog'i — IlmHamroh