Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 Express'da qanday edi
- 2. ClassSerializerInterceptor
- 2.1 G'oya
- 2.2 Javob klassi
- 2.3 Controller va natija
- 2.4 Global ulash
- 3. Plain obyekt tuzog'i
- 3.1 Nega /plain buzildi
- 3.2 Real hayotda plain obyekt qayerdan keladi
- 3.3 Himoya yo'llari
- 4. Javob sxemasi va response DTO
- 4.1 Zod bilan
- 4.2 Response DTO
- 4.3 Qaysi yo'lni tanlash
- 4.4 vazifalar-api da
- 5. Hujumchi nigohi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): dekoratorni tanlang
- 2-mashq (o'rta): javob sxemasi modeli
- 3-mashq (qiyin): guruhlar modeli
- 8. Loyiha qadami: 415 guard'da va commit
- 8.1 Muammo
- 8.2 src/umumiy/json-tana.guard.ts
- 8.3 Ulash
- 8.4 Tekshiruv
- 8.5 Commit
- 9. Real ishda
- Xulosa
- Manbalar
Javobni serializatsiya qilish NestJS'da: ClassSerializerInterceptor, @Exclude va plain obyekt tuzog'i
Qisqacha: Kirishni tekshirish yetarli emas — chiqish ham nazorat qilinadi:
parolHashkabi 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 —@Excludejim e'tiborsiz qoladi vaparolHashjavobga 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@SerializeOptionsni 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-apida 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:
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"] })— faqatadminguruhi 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;@Exposeuni qo'shadi.
2.3 Controller va natija
@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:
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:parolHashyo'q,oxirgiKirishyo'q (guruh so'ralmagan),ism— katta harf,qisqaEmailqo'shildi./plain—parolHashjavobda.ismham o'zgarmagan,qisqaEmailyo'q. Ya'ni hech qaysi dekorator ishlamadi./admin—@SerializeOptions({ groups: ["admin"] })bilanoxirgiKirishchiqdi./type— handler yana oddiy obyekt qaytardi, lekin@SerializeOptions({ type })interceptor'ga "buni avval shu klassga aylantir" dedi. Natija/klassbilan 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:
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):
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:
{ id: 1, email: 'aziz@example.com' }
{ id: 1, email: 'aziz@example.com', parolHash: 'x' }
ObjectBir 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).
pgdrayveriningrows— oddiy obyektlar.- Redis keshidan
JSON.parseqilingan 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:
FoydalanuvchiJavobidaparolHash@Exclude()bilan belgilangan va interceptor ulangan. Handler:const f = await this.ombor.top(id); return { ...f, toliqIsm: f.ism + " " + f.familiya };.ombor.topklass nusxasini qaytaradi.parolHashchiqadimi?
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:
const JavobSxemasi = z.object({
id: z.number(), email: z.string(), ism: z.string(),
});
@Get("zod") zod() { return JavobSxemasi.parse(bazadan); }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:
- 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.
- 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. - 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/1javobi 60 soniya Redis'da keshlanadi. Birinchi so'rovda handler klass nusxasini qaytaradi va@Excludeishlaydi. 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
- 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,SELECTda aniq ustunlar). - Kesh orqali oqish. Birinchi so'rov klass nusxasini qaytaradi (
@Excludeishlaydi), keyingisi Redis keshidan oddiy obyektni — maxfiy maydon bilan. Himoya — keshga allaqachon serializatsiya qilingan javobni yozish yoki sxemani keshdan keyin qo'llash. - Guruhni almashtirish.
?guruh=adminkabi parametr bilan serializatsiya guruhini mijoz tanlasa — admin maydonlari ochiladi. Himoya — guruh faqat serverda, foydalanuvchi roliga qarab aniqlanadi. - Xato javobida oqish.
500javobiga xato obyekti yoki stack tushsa — ichki yo'llar, SQL so'rovi ko'rinadi. Himoya — Problem Details filtri faqat umumiydetailqaytaradi, 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;
excludeExtraneousValuesbilan — 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
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:
{ id: 1, email: 'aziz@example.com', ism: 'Aziz' }
{ id: 1, email: 'aziz@example.com', ism: 'Aziz' }
javob sxemasi: id — son emasIkkinchi 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
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:
{ 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.
// 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()—SetMetadatabilan belgi qo'yadigan dekorator. U 25-qismdagijsonKerakroute middleware'ining o'rni: "shu marshrutga JSON tana kerak".canActivate— belgi bo'lsa, o'shajsonTanaKerak(req)ni chaqiradi (fayl o'zgarmaydi). Xato tashlansa — so'rov filtrga ketadi va 415 Problem Details oladi.return true— guardfalseqaytarsa, Nest o'zi403 Forbiddenberadi. Bizga boshqa status kerak — shuning uchun rad etish xato tashlash orqali.Reflector.createDecoratoremas — 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:
+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:
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):
--- 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:
$ 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:
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 useryokireturn { ...user }— darhol savol: "parol hash'i chiqmaydimi?". - Testlarda faqat "javobda
emailbor" 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 loyihalaridamain.tsda yoqilgan. Uni ko'rsangiz, ombor qatlami klass nusxasi qaytarishini tekshiring. - Intervyu: "
@Excludeqachon 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,
parolHashchiqadi. - Himoya — ruxsat ro'yxati:
@SerializeOptions({ type }),excludeExtraneousValues, javob sxemasi (Zodz.objectstrip),SELECTda aniq ustunlar. - Global ulash —
useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))yokiAPP_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 qadaminesttarmog'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
- NestJS hujjati — Serialization (
ClassSerializerInterceptor,@Exclude,@Expose,@SerializeOptions): https://docs.nestjs.com/techniques/serialization - class-transformer —
instanceToPlain,excludeExtraneousValues, groups: https://github.com/typestack/class-transformer - OWASP API Security Top 10 (2023) — API3:2023 Broken Object Property Level Authorization: https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
- Kanon (IlmHamroh, 2026-10-27): serializatsiya tajribasi (NestJS 12.2.0, Vitest ichida),
vazifalar-apitekshiruvi — tip/lint/Vitest 117/118/build, jonli 9 so'rov × 3 rejim. Bazasiz test yurgizish (75/81) va formatlangan fayllar tekshiruvi — biz ishga tushirdik (2026-11-02, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!