Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Klient — provider
- 2.1 Token va async fabrika
- 2.2 Fail-fast — haqiqiy chiqish
- 3. Controller'lar — sinxron, bog'liqliklar — async
- 3.1 Nega controller'lar async bo'la olmaydi
- 3.2 JS modelida
- 4. IntegratsiyaModule.forRoot
- 4.1 Tuzilma
- 4.2 Redis subscriber — alohida ulanish
- 4.3 Yopish
- 4.4 Yangi xizmat qo'shish — besh savolga javob
- 5. Tayyor Nest paketlari
- 5.1 @nestjs/axios va fetch
- 5.2 Mailer va Telegram modullari
- 6. 29-qismdan qolgan xato: obunadan chiqish sahifasi
- 6.1 Xato
- 6.2 Tuzatish — faqat shu sahifaga CSP
- 7. Topilma: test ichidagi klass va Vite
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qaysi bosqich?
- 2-mashq (o'rta): fail-fast konteyner
- 3-mashq (qiyin): CSP tekshiruvchisi
- 10. Loyiha qadami: IntegratsiyaModule
- 11. Real ishda
- Xulosa
- Manbalar
Tashqi xizmatlarni Nest modullariga o'rash: dinamik modul, async fabrika va fail-fast
Qisqacha: Tashqi xizmat (S3, SMTP, Redis, Telegram, tashqi API) klienti Nest'da provider bo'ladi: token +
useFactory(async bo'lishi mumkin) +inject: [CONFIG]. Fabrika ichida ulanish tekshiriladi (S3 bucket, SMTPverify(), Redisconnect()) — xato bo'lsa ilova umuman ishga tushmaydi (fail-fast). Yopish —onApplicationShutdownhook'ida. Bir nechta xizmat bitta dinamik modulga (IntegratsiyaModule.forRoot) yig'iladi; controller'lar ro'yxati esa config bo'yicha sinxron tanlanadi, chunki Nest modul metama'lumoti statik. Tayyor paketlar (@nestjs/axios,@nestjs-modules/mailer, Telegram modullari) bor, lekinvazifalar-apiularsiz — Nodefetch, nodemailer va grammY to'g'ridan.
Bu darsda
- Tashqi klientni token + async fabrika bilan provider qilasiz va
injectbilan config'ni olasiz. - Fail-fast nima ekanini va
abortOnErrorqanday ta'sir qilishini haqiqiy chiqishda ko'rasiz. - Config bo'yicha controller'larni yoqadigan dinamik modul yozasiz.
@nestjs/axios, mailer va Telegram modullarining o'rnini va kanon nega ularsiz ekanini bilasiz.- 29-qismdan qolgan obunadan chiqish sahifasi xatosi Nest'da qanday tuzatilganini ko'rasiz.
Oldin bilishingiz kerak: Dynamic modullar, Custom provider'lar, Lifecycle hook'lar, @nestjs/config, Email yuborish: SMTP va Nodemailer, grammY bilan bot yozish.
O'tgan darsda navbat producer'i — Navbatlar — provider bo'ldi, lekin u qayerda va qanday yaratilishini keyinga qoldirdik. Fayl controller'i ham hali modulga ulanmagan. Bu dars ularning hammasini bitta modulga yig'adi.
1. Nega bu kerak?
vazifalar-api 29-qismdan keyin ko'p tashqi xizmatga bog'langan: MinIO (S3), SMTP (Mailpit), Redis (kesh, Pub/Sub, navbat), Telegram Bot API, Web Push, SMS provayder, tashqi webhook'lar. Har birining klienti bor va har biri bir xil savollarni beradi:
- Qachon yaratiladi? Ilova ishga tushganda, bir marta.
- Sozlama qayerdan? Config'dan (
S3_BUCKET,SMTP_HOST…). - Ulanib bo'lmasa? Ilova ishga tushmasin — birinchi foydalanuvchi xatoni ko'rishidan oldin.
- Qanday yopiladi? SIGTERM'da ulanishlar to'g'ri yopilsin.
- Testda? Haqiqiy xizmat o'rniga soxtasi berilsin.
29-qismda bu savollarga server.ts da qo'lda javob berilgan: hamma klientlar ketma-ket yaratilib, routerlarga uzatilgan. Nest'da bularning har biriga tayyor joy bor: provider, fabrika, lifecycle hook va dinamik modul.
O'xshatish: to'y uchun xizmatlar — musiqachilar, oshpaz, fotograf. To'ydan oldin har biriga qo'ng'iroq qilib, kelishini tasdiqlaysiz (fail-fast). Fotograf kelmasa — to'yni kechiktirasiz, mehmonlar kelgandan keyin emas. To'y tugagach har biri bilan hisob-kitob qilib, xayrlashasiz (yopish).
2. Klient — provider
2.1 Token va async fabrika
Pochta klienti kanonda shunday provider (src/auth/auth.module.ts, qatorlar bo'lingan):
const pochtaProvider: Provider = {
provide: POCHTA,
inject: [CONFIG],
useFactory: async (c: Config): Promise<Pochta | null> => {
if (c.SMTP_HOST === undefined) return null;
const p = new Pochta({
host: c.SMTP_HOST,
port: c.SMTP_PORT,
secure: c.SMTP_SECURE,
requireTLS: c.NODE_ENV === "production" && !c.SMTP_SECURE,
// … user/pass bo'lsa, from
});
await p.tekshir();
return p;
},
};To'rtta narsa (Custom provider'lar):
provide: POCHTA—Symboltoken. Klass emas, chunkiPochta | null— interfeysga yaqin tip.inject: [CONFIG]— fabrikaga config beriladi (@nestjs/config).async— Nest fabrika Promise'ini kutadi va keyingi provider'larni shundan keyin yaratadi.await p.tekshir()— ichida nodemailer'ningverify(): SMTP serverga ulanib, salomlashadi (Email yuborish: SMTP va Nodemailer). Server yo'q — xato — ilova ishga tushmaydi.
SMTP_HOST yo'q bo'lsa — null: pochta o'chiq, ilova ishlayveradi (DEV rejimi). Bu ataylab tanlangan: sozlanmagan xizmat — "yo'q", sozlangan, lekin ishlamaydigan xizmat — xato.
Ulanishni tekshirishning yana bir joyi bor — lifecycle hook (hayot sikli ilgagi) onModuleInit. Ikkalasi ham port ochilishidan oldin ishlaydi. Farqi — klient qachon "tayyor" deb hisoblanishida:
| Qayerda | Klient boshqalarga qachon beriladi | Kamchiligi |
|---|---|---|
| async fabrika (kanon) | faqat tekshiruvdan keyin | — |
klass + onModuleInit |
darhol, hali ulanmagan holda | boshqa provider constructor'da uni ishlatsa — ulanmagan klient |
Fabrika "yarim tayyor" obyektni umuman chiqarmaydi: POCHTA tokeni ostida yo tekshirilgan klient, yo null bo'ladi. Shu sabab tashqi klientlar uchun fabrika qulayroq.
2.2 Fail-fast — haqiqiy chiqish
Demo modul yozdik: PochtaModule.forRoot({ smtp }). Fabrika smtp === "yomon" bo'lsa xato tashlaydi, aks holda klient qaytaradi. Controller esa smtp berilgandagina qo'shiladi (biz ishga tushirdik, NestJS 12.2.0):
@Module({})
class PochtaModule {
static forRoot(s: Sozlama): DynamicModule {
return {
module: PochtaModule,
// controller ro'yxati — SINXRON, sozlama bo'yicha
controllers: s.smtp === undefined ? [] : [PochtaController],
providers: [Yopuvchi, {
provide: POCHTA,
useFactory: async () => {
// verify() o'xshashi: ulanib bo'lmasa — ilova TURMAYDI
if (s.smtp === "yomon") throw new Error("SMTP ulanmadi");
await new Promise((ok) => setTimeout(ok, 10));
return { host: s.smtp ?? "yo'q" };
},
}],
};
}
}Uch sozlama bilan, abortOnError: false. Har sozlamadan keyin yopildi qatori bor-yo'qligiga qarang — u ilova haqiqatan ishga tushganining belgisi:
smtp=undefined: GET /pochta → 404
yopildi: close()
smtp=127.0.0.1:1025: GET /pochta → 200
yopildi: close()
smtp=yomon: ishga tushmadi — SMTP ulanmadi- Sozlanmagan — controller yo'q, marshrut 404. Ilova ishlaydi.
- Sozlangan — 200.
- Ishlamaydigan —
NestFactory.createxato bilan yiqildi, port ochilmadi.YopuvchiningonApplicationShutdowni ham chaqirilmadi — ilova hech qachon "ishga tushmagan".
abortOnError sukut qiymatida (true) xuddi shu "yomon" holat:
[Nest] LOG [NestFactory] Starting Nest application...
[Nest] ERROR [ExceptionHandler] Error: SMTP ulanmadi
at InstanceWrapper.useFactory [as metatype] (file:///C:/Users/…/t-modul2.js:50:35)
… (Nest ichki stack qatorlari)
exit 1Jarayon raqami, vaqt va stack qisqartirildi. Sukut bo'yicha Nest xatoni logga yozadi va process.exit(1) qiladi. Xato catch ga yetmaydi. vazifalar-api abortOnError: false ishlatadi: xato Promise orqali main.ts ga keladi va config xatosi 78 kodi bilan, boshqasi stack bilan chiqadi (Monorepo, standalone ilova va CLI buyruqlar darsidagi kabi).
Tekshirib ko'ring: Nega SMTP tekshiruvini birinchi email yuborilganda emas, ishga tushishda qilamiz?
Javob
Ishga tushishda xato — deploy paytida ko'rinadi: yangi versiya ko'tarilmaydi, eskisi ishlashda davom etadi (Health check'lar bilan birga). Birinchi email paytida xato — foydalanuvchi "parolni tiklash" bosganda, ehtimol kechasi, hech kim kuzatmayotganda. Fail-fast xatoni eng arzon paytga ko'chiradi.
3. Controller'lar — sinxron, bog'liqliklar — async
3.1 Nega controller'lar async bo'la olmaydi
Fabrika async — S3 bucket'ni tayyorlash vaqt oladi. Lekin controller'lar ro'yxati — controllers: [...] — modul metama'lumotida. Nest marshrutlarni modul daraxtini yig'ishda o'qiydi, provider'lar yaratilishidan oldin. Shuning uchun "S3 ulandimi — ulangan bo'lsa controller qo'shilsin" degan qaror async bo'la olmaydi.
Yechim — ikki bosqich:
- Sinxron: config qiymatlariga qarab qaysi xizmat yoqilgan —
yoqilganlar(config). Shu ro'yxatdan controller'lar. - Async: yoqilgan xizmatlar uchun fabrikalar — ulanish va tekshiruv.
Config sinxron o'qiladi (Zod sxemasi — Config validatsiyasi va namespace'lar). Ulanish esa async. Ikkalasini ajratish — shu darsning asosiy g'oyasi.
flowchart TB
C["config (Zod, sinxron)"] --> Y["yoqilganlar(config)"]
Y --> K["controllers: [...] — modul metama'lumoti"]
Y --> F["async fabrikalar"]
F --> S3["S3: tayyorla()"]
F --> SM["SMTP: verify()"]
F --> R["Redis sub: connect()"]
S3 --> OK{"hammasi o'tdimi?"}
SM --> OK
R --> OK
OK -- ha --> L["listen: ilova tayyor"]
OK -- yo'q --> X["ishga tushmaydi (fail-fast)"]Diagrammaga qarang: controllers tuguni — sinxron qaror, qaysi marshrutlar bo'ladi. Fabrikalar tarmog'i — async, ular ishlay oladimi. Bitta fabrika yiqilsa, port umuman ochilmaydi.
3.2 JS modelida
Kanondagi yoqilganlar ning qisqa modeli (brauzerda ishlaydi):
// IntegratsiyaModule.forRoot modeli: config → qaysi xizmat yoqiladi
// (sinxron) → controller'lar ro'yxati; bog'liqliklar — async fabrika
function yoqilganlar(c) {
const pg = c.SAQLASH === "postgres" || c.SAQLASH === "prisma";
return {
fayllar: pg && c.S3_BUCKET !== undefined,
navbat: pg && c.REDIS_URL !== undefined,
jonli: pg,
};
}
const CONTROLLERLAR = {
fayllar: ["IlovalarController", "FayllarController"],
navbat: ["MuddatController"],
jonli: ["OqimController"],
};
function forRoot(c) {
const y = yoqilganlar(c);
const controllers = Object.keys(y)
.filter((k) => y[k])
.flatMap((k) => CONTROLLERLAR[k]);
return { controllers };
}
const rejimlar = {
"xotira (DEV)": { SAQLASH: "xotira" },
"postgres, Redis'siz": { SAQLASH: "postgres", S3_BUCKET: "vz" },
"postgres + Redis": {
SAQLASH: "postgres", S3_BUCKET: "vz", REDIS_URL: "redis://x",
},
};
for (const [nom, c] of Object.entries(rejimlar)) {
const { controllers } = forRoot(c);
console.log(`${nom}: ${controllers.join(", ") || "(yo'q)"}`);
}Konsolda:
xotira (DEV): (yo'q)
postgres, Redis'siz: IlovalarController, FayllarController, OqimController
postgres + Redis: IlovalarController, FayllarController, MuddatController, OqimControllerTekshirib ko'ring: Xizmat sozlanmagan bo'lsa, controller'ni baribir qo'shib, har so'rovga 503 qaytarsa bo'lmasmidi? Nega kanon yo'lni umuman yo'q qiladi (404)?
Javob
Ikkalasi ham mumkin, ma'nosi farq qiladi. 503 — "xizmat bor, lekin hozir ishlamayapti, keyinroq urining": mijoz qayta urinadi. 404 — "bunday yo'l yo'q": mijoz urinmaydi. Sozlanmagan xizmat vaqtincha emas, doimiy holat — 29-qismda uning routeri ham umuman ulanmasdi va testlar 404 kutadi. Ustiga, yo'q controller Swagger hujjatida ham ko'rinmaydi: hujjat ilovaning haqiqiy imkoniyatini ko'rsatadi. 503 esa sozlangan, lekin ishga tushgandan keyin uzilib qolgan xizmat uchun to'g'ri — buni health tekshiruvi ko'rsatadi.
Xotira rejimida integratsiyalarning hech biri yo'q — 25-qismdagi oddiy API qoladi. Redis'siz — navbat yo'q, chunki BullMQ Redis talab qiladi. Model soddalashtirilgan: kanonda yana authYoqilgan, S3 kalitlari, Telegram bot nomi tekshiriladi.
4. IntegratsiyaModule.forRoot
4.1 Tuzilma
Kanon modulining boshi va bitta xizmat (fayllar) bo'limi — qisqartirilgan, qatorlar bo'lingan:
@Global()
@Module({})
export class IntegratsiyaModule {
static forRoot(
o: IntegratsiyaSozlamasi,
configRejimi?: ConfigRejimi,
): DynamicModule {
const y = configRejimi === undefined
? undefined
: yoqilganlar(configRejimi);
const controllers: Type[] = [];
const providers: Provider[] = [
XabarchiTinglovchi, IntegratsiyaYopuvchi,
];
const qiymat = (token: symbol, v: unknown) =>
providers.push({ provide: token, useValue: v ?? null });
// --- fayllar (S3/MinIO): bucket yo'q bo'lsa yaratiladi
if (o.fayllar !== undefined || y?.fayllar === true) {
controllers.push(IlovalarController, FayllarController);
}
if (o.fayllar !== undefined || y?.fayllar !== true) {
qiymat(FAYLLAR, o.fayllar);
} else {
providers.push({
provide: FAYLLAR,
inject: [CONFIG, SAQLASH],
useFactory: async (c: Config, s: Saqlash) => {
const ombor = new FaylOmbori({ /* endpoint, region,
kalit, sir, bucket — config'dan */ });
await ombor.tayyorla();
return { ombor, pool: pool(s),
maksHajm: c.FAYL_MAKS_MB * 1024 * 1024 };
},
});
}
// … bildirishnoma, obuna, telegram, webhooklar, jonli (SSE),
// navbat — xuddi shu naqshda
return {
module: IntegratsiyaModule, controllers, providers,
exports: [FAYLLAR, BILDIRISHNOMA, OBUNA_KALITI, TELEGRAM_ULASH,
WEBHOOKLAR, JONLI, NAVBATLAR],
};
}
}Uch yo'l har xizmat uchun:
| Kirish | Natija |
|---|---|
o.fayllar berilgan (test) |
controller bor, provider — testning tayyor qiymati (useValue) |
config'da yoqilgan (main.ts) |
controller bor, provider — async fabrika (tayyorla()) |
| hech biri | controller yo'q, provider — null |
Testlar haqiqiy S3 o'rniga soxta FaylOmbori beradi — e2e testlar darsidagi "test bergan chegaralar" g'oyasi. ilovaYarat({ fayllar: {...} }) shu o ga keladi.
4.2 Redis subscriber — alohida ulanish
SSE uchun Redis Pub/Sub kerak. node-redis'da obuna (subscribe) rejimidagi ulanish boshqa buyruq bajara olmaydi — shuning uchun alohida ulanish (Pub/Sub va Redis Streams). Fabrika (qisqartirilgan):
useFactory: async (s: Saqlash, log: Log): Promise<JonliTayyor> => {
const tarqatuvchi = s.redis === undefined
? undefined : new RedisTarqatuvchi(s.redis);
const hub = new SseHub(tarqatuvchi === undefined
? {} : { tarix: tarqatuvchi.tarix() });
const sub = s.redis?.duplicate();
if (sub !== undefined) {
sub.on("error", (err) => {
log.error({ err }, "redis (jonli sub): ulanish xatosi");
});
await sub.connect();
await RedisTarqatuvchi.tingla(sub, hub);
}
return { hub, yop: () => { hub.yop(); sub?.destroy(); } };
},s.redis — 27-qismdagi kesh klienti, allaqachon bor. duplicate() — xuddi shu sozlamali yangi ulanish: manzil va parol qayta yozilmaydi, faqat yangi TCP ulanish ochiladi. Fabrika yop funksiyasini ham qaytaradi — yopuvchi uni chaqiradi. Ulanishni kim ochgan bo'lsa, o'sha yopadi: bu qoida resurslarni "yetim" qoldirmaydi.
4.3 Yopish
@Injectable()
class IntegratsiyaYopuvchi
implements BeforeApplicationShutdown, OnApplicationShutdown {
constructor(
@Inject(JONLI_TAYYOR) private readonly jonli: JonliTayyor | null,
@Inject(NAVBATLAR)
private readonly navbat: NavbatSozlamasi | null,
@Inject(LOG) private readonly log: Log,
) {}
beforeApplicationShutdown(): void {
this.jonli?.yop();
}
async onApplicationShutdown(): Promise<void> {
await this.navbat?.navbatlar.yop().catch((err: unknown) => {
this.log.warn({ err }, "navbat yopilmadi");
});
}
}app.close() chaqirilganda Nest ilgaklarni qat'iy tartibda chaqiradi (Lifecycle hook'lar). Kanonda har qadamda kim nima qiladi:
| Tartib | Ilgak | Kanonda |
|---|---|---|
| 1 | onModuleDestroy |
— |
| 2 | beforeApplicationShutdown |
SSE oqimlari va Redis subscriber yopiladi |
| 3 | (HTTP server yopiladi) | kanonda HTTP ulanishlar bundan oldin, o'z signal ishlovchisida yopilgan |
| 4 | onApplicationShutdown |
navbat producer'i; AuthYopuvchi — o'z pool'i |
Tartib ataylab: beforeApplicationShutdown — avval SSE ulanishlari va subscriber (ochiq HTTP oqimlar serverni yopilishdan ushlab turmasin), keyin onApplicationShutdown — navbat producer'i. Navbat yopilmasa — faqat ogohlantirish: yopish paytidagi xato boshqa resurslarning yopilishiga to'sqinlik qilmasin.
Tekshirib ko'ring: Nega
IntegratsiyaModule@Global()?
Javob
Uning eksportlari (FAYLLAR, JONLI, NAVBATLAR …) boshqa modullarga ham kerak: AuthModule pochtani, VazifalarModule hodisalarni jonli oqimga. Har birida imports: [IntegratsiyaModule.forRoot(...)] yozish — forRoot ni har safar sozlama bilan chaqirish degani. Global modul bir marta, AppModule da ulanadi (Modullar). Kanon @Global ni kam ishlatadi: faqat BogliqliklarModule va shu modul.
4.4 Yangi xizmat qo'shish — besh savolga javob
Darsning boshida har tashqi xizmat beradigan besh savolni yozgandik. Endi har biriga Nest'dagi aniq joyni qo'yamiz. Ertaga vazifalar-api ga, masalan, tashqi tarjima API'si qo'shilsa, shu ro'yxat bo'yicha ishlaysiz:
| Savol | Nest'dagi javob | Kanondagi misol |
|---|---|---|
| Qachon yaratiladi? | provider fabrikasi — ishga tushishda bir marta | FAYLLAR fabrikasi |
| Sozlama qayerdan? | inject: [CONFIG], Zod sxemasiga yangi maydonlar |
S3_BUCKET, SMTP_HOST |
| Ulanib bo'lmasa? | fabrikada tekshiruv, xato — ilova turmaydi | tayyorla(), verify(), connect() |
| Qanday yopiladi? | yopuvchi klassda lifecycle hook | IntegratsiyaYopuvchi |
| Testda? | forRoot(o) dagi tayyor qiymat (useValue) |
ilovaYarat({ fayllar }) |
Bitta savolga javob yo'q bo'lsa — xizmat yarim ulangan. Eng ko'p unutiladigani — to'rtinchisi: ilova ishlaydi, testlar o'tadi, faqat deploy paytida jarayon yopilmay "osilib" qoladi.
5. Tayyor Nest paketlari
Nest ekotizimida tashqi xizmatlar uchun tayyor modullar bor. Ishga kirsangiz, ularni ko'rasiz:
| Paket | Nima beradi | Kanonda |
|---|---|---|
@nestjs/axios (HttpModule) |
HttpService — axios o'rami, javob — RxJS Observable |
yo'q — Node fetch |
@nestjs-modules/mailer |
MailerModule.forRoot, shablonlar (Handlebars, Pug) |
yo'q — nodemailer + o'z Pochta |
nestjs-telegraf / @grammyjs/nestjs |
bot handler'lari dekorator bilan (@Update, @Command) |
yo'q — grammY to'g'ridan |
| SMS provayder modullari | odatda o'zingiz yozasiz | o'z EskizProvayder |
5.1 @nestjs/axios va fetch
HttpService.get(url) — Observable qaytaradi. Uni Promise qilish uchun firstValueFrom(...) yoziladi (Interceptor'lar va RxJS). Node 24 da fetch ichida bor, AbortSignal.timeout bilan timeout ham oddiy (Built-in fetch va undici). Kanon qarori: @nestjs/axios — qo'shimcha ikki paket (axios va o'ram) va RxJS'ga o'tish, foydasi esa kam. Tashqi webhook'lar 25-qismdagi webhookXabarchi (fetch + timeout + SSRF himoyasi) bilan qoldi.
HttpModule qachon foydali: jamoa axios interceptor'lariga o'rgangan bo'lsa, yoki eski Nest kodi shunda yozilgan bo'lsa. Yangi kodda Node fetch yetarli.
5.2 Mailer va Telegram modullari
@nestjs-modules/mailer — nodemailer ustidagi o'ram: MailerModule.forRootAsync({ useFactory }) va mailerService.sendMail({ template, context }). Kanonda 29-qismdagi Pochta klassi (nodemailer + Handlebars, tranzaksion xat qoidalari) bor va u provider sifatida ulanadi — o'ram yangi narsa bermaydi.
Telegram uchun nestjs-telegraf (Telegraf kutubxonasi) va grammY'ning Nest o'rami bor. Kanondagi bot (29-qism, grammY) — alohida jarayon (npm run bot), API ichida emas. API faqat ulash havolasini beradi (TelegramController). Shuning uchun bot'ni Nest moduliga o'rashga ehtiyoj yo'q.
Bu paketlarning birortasini kanon ham, biz ham sinamadik — jadval rasmiy hujjatlarga asoslangan (2026-11).
6. 29-qismdan qolgan xato: obunadan chiqish sahifasi
6.1 Xato
29-qismda email'ga "obunadan chiqish" havolasi qo'shilgan (Email yetkazib berish (deliverability)): GET /v1/obuna/:token — tasdiqlash sahifasi, unda "Ha, o'chirish" tugmali forma; POST — obunani o'chiradi. Gmail'ning "one-click" tugmasi POST ni o'zi yuboradi va ishlaydi.
Lekin foydalanuvchi havolani bosib, sahifadagi tugmani bossa — hech narsa bo'lmaydi. Sabab — API'ning qat'iy CSP sarlavhasi (helmet): form-action 'none'. Brauzer sahifadagi formani yuborishni to'sadi. 29-qism testlari buni ko'rmagan: supertest brauzer emas, CSP'ni bajarmaydi. 29-qismning o'sha darsida bu haqda izoh bor.
Kanon kodni Nest controller'iga ko'chirayotganda, xatoni topdi va tuzatdi.
6.2 Tuzatish — faqat shu sahifaga CSP
@Controller("obuna")
@Ochiq()
export class ObunaController {
// … constructor, #och(token)
@Get(":token")
@Header("Content-Type", "text/html; charset=utf-8")
// sahifa o'z formasini yubora olsin (API'ning qat'iy CSP'si
// form-action 'none'); inline uslub — faqat shu sahifa
@Header("Content-Security-Policy",
"default-src 'none'; style-src 'unsafe-inline'; " +
"form-action 'self'; frame-ancestors 'none'; base-uri 'none'")
sahifa(@Param("token") token: string): string {
this.#och(token);
return SAHIFA;
}
// … @Post(":token") @HttpCode(200) bekor(...)
}@Header controller'da qo'yilgan sarlavha global helmet qo'ygan sarlavhani almashtiradi — javob yuborilishidan oldin Nest uni yozadi. Qolgan hamma javoblarda form-action 'none' qoladi. form-action 'self' — forma faqat sahifaning o'z manziliga yuborila oladi. style-src 'unsafe-inline' — faqat shu sahifa uchun, inline uslub ishlashi uchun. Kanon izohi shuni ham aytadi: 29-qismda uslub ham to'silgan edi.
Kanon testi (test/nest-integratsiya.test.ts):
test("one-click obuna sahifasi: CSP form-action 'self', API " +
"qolgan javoblari — form-action 'none'", async () => {
const t = obunaTokeni(obunaKaliti, 1, "email");
const r = await request(http).get(`/v1/obuna/${t}`).expect(200);
expect(r.headers["content-type"]).toMatch(/^text\/html/);
expect(r.headers["content-security-policy"])
.toContain("form-action 'self'");
const h = await request(http).get("/health/live");
expect(h.headers["content-security-policy"])
.toContain("form-action 'none'");
});Test sarlavhani tekshiradi — brauzer xulqini emas. Haqiqiy brauzerda (Playwright) formani bosib ko'rish kanon tomonidan sinalmagan. Saboq 29-qismdagidek: brauzerda ishlaydigan narsani brauzerda sinang.
Shunga qaramay, sarlavha testi foydasiz emas. U ikki tomonni qotiradi: obuna sahifasida forma ochiq, qolgan hamma javobda — yopiq. Kimdir kelajakda global helmet sozlamasini o'zgartirib, form-action ni butun API uchun bo'shatsa, test darhol yiqiladi. Brauzer testi "ishlaydimi?" savoliga, sarlavha testi esa "himoya joyidami?" savoliga javob beradi — ikkalasi bir-birini to'ldiradi.
7. Topilma: test ichidagi klass va Vite
Telegram bot testida (test/telegram-bot.test.ts) 29-qismda test ichida kichik klass bor edi: class Sekin extends PgVazifaManbai { maydon = 0; override … }. node --test da ishlagan. Vitest'da esa:
SyntaxError: Unexpected token 'const'Sabab: Vite (oxc) experimentalDecorators sozlamasi bilan blok ichidagi klass maydonlarini o'girganda noto'g'ri kod chiqardi. Ilova kodida emas — faqat test faylida. Tuzatish — klassni modul darajasiga (test funksiyasidan tashqariga) ko'chirish. Xato matni kanon hisobotidan; uni biz qayta yurgizmadik.
Bu unit testlar darsidagi saboqning yana bir ko'rinishi: Vitest va tsc kodni har xil o'giradi. Kodda g'alati SyntaxError ko'rsangiz va kod to'g'ri bo'lsa — o'giruvchini gumon qiling.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Ilova ishga tushdi, birinchi so'rovda "ECONNREFUSED" | fabrika ulanishni tekshirmadi | fabrikada verify()/connect() |
ExceptionHandler va exit 1, catch ishlamadi |
abortOnError sukut — true |
abortOnError: false |
| Controller config'ga qarab qo'shilmaydi | ro'yxatni async fabrikadan olishga urinish | sinxron yoqilganlar(config) |
| SIGTERM'da jarayon osiladi | subscriber/navbat yopilmagan | beforeApplicationShutdown/onApplicationShutdown |
| Brauzerda forma ishlamaydi, test yashil | CSP form-action 'none' |
sahifaga alohida CSP, brauzerda sinash |
| Boshqa provider ulanmagan klientni oladi | tekshiruv onModuleInit da, klient esa constructor'da ishlatiladi |
tekshiruvni fabrikaga ko'chiring |
| SSE ishlaganda Redis buyruqlari yiqiladi | subscriber va oddiy buyruqlar bitta ulanishda | duplicate() bilan alohida ulanish |
| Global modul ikki marta sozlanadi | forRoot bir nechta modulda chaqirilgan |
@Global() va faqat AppModule da |
9. Mashqlar
Mashqlar kurs/mashqlar/30/64-tashqi-xizmatlar/ da.
1-mashq (oson): qaysi bosqich?
To'ldiring: [:sinxron] yoki [:async].
- Config'ni o'qish va Zod tekshiruvi —
- S3 bucket'ni tayyorlash (
tayyorla()) — - Controller'lar ro'yxatini tanlash —
- SMTP
verify()—
Yechim
- sinxron — config
process.envdan darhol o'qiladi; xato bo'lsa darholConfigXatosi. - async — tarmoq so'rovi.
- sinxron — modul metama'lumoti statik, Nest uni provider'lardan oldin o'qiydi.
- async — SMTP serverga ulanish.
2-mashq (o'rta): fail-fast konteyner
Paketsiz model. ishgaTushir(fabrikalar) — { nom: async () => klient } obyekti. Hamma fabrikalarni ketma-ket chaqirsin; bittasi xato bersa — oldin yaratilganlarning yop() metodini teskari tartibda chaqirib, "ishga tushmadi: <nom> — <xabar>" qaytarsin. Hammasi o'tsa — "tayyor: <nomlar>". Ishora: try/catch va toReversed() (Nusxa bilan o'zgartirish).
Yechim
async function ishgaTushir(fabrikalar) {
const tayyor = [];
for (const [nom, f] of Object.entries(fabrikalar)) {
try {
tayyor.push([nom, await f()]);
} catch (e) {
for (const [n, k] of tayyor.toReversed()) {
k.yop();
console.log(` yopildi: ${n}`);
}
return `ishga tushmadi: ${nom} — ${e.message}`;
}
}
return `tayyor: ${tayyor.map(([n]) => n).join(", ")}`;
}
const klient = () => ({ yop() {} });
const yaxshi = {
redis: async () => klient(),
s3: async () => klient(),
};
const yomon = {
redis: async () => klient(),
s3: async () => klient(),
smtp: async () => { throw new Error("ECONNREFUSED"); },
};
console.log(await ishgaTushir(yaxshi));
console.log(await ishgaTushir(yomon));Konsolda:
tayyor: redis, s3
yopildi: s3
yopildi: redis
ishga tushmadi: smtp — ECONNREFUSEDMuhim qism — xato bo'lganda oldin ochilganlarni yopish. Aks holda Redis ulanishi ochiq qolib, jarayon tugamaydi. Teskari tartib — keyin ochilgan narsa birinchi yopiladi (u oldingisiga tayanishi mumkin).
3-mashq (qiyin): CSP tekshiruvchisi
cspOl(sarlavha) — CSP satrini { direktiva: [qiymatlar] } obyektiga aylantirsin (; bilan ajratilgan, har qismda birinchi so'z — direktiva). formaYuboriladimi(csp, sahifa, manzil) — form-action bo'yicha qaror: yo'q bo'lsa true, 'none' bo'lsa false, 'self' bo'lsa manzil sahifa bilan bir origin'dami. API sarlavhasi va obuna sahifasi sarlavhasi bilan sinang. Ishora: new URL(...).origin (URL va URLSearchParams).
Yechim
function cspOl(sarlavha) {
return Object.fromEntries(
sarlavha.split(";").map((q) => q.trim()).filter(Boolean)
.map((q) => {
const [d, ...qiymatlar] = q.split(/\s+/);
return [d, qiymatlar];
}),
);
}
function formaYuboriladimi(csp, sahifa, manzil) {
const fa = csp["form-action"];
if (fa === undefined) return true;
if (fa.includes("'none'")) return false;
const origin = (u) => new URL(u, sahifa).origin;
const bir = origin(manzil) === origin(sahifa);
return fa.includes("'self'") && bir;
}
const API = "default-src 'none'; base-uri 'none'; " +
"form-action 'none'; frame-ancestors 'none'";
const OBUNA = "default-src 'none'; style-src 'unsafe-inline'; " +
"form-action 'self'; frame-ancestors 'none'; base-uri 'none'";
const sahifa = "https://api.example.com/v1/obuna/abc";
for (const [nom, s] of [["API", API], ["obuna", OBUNA]]) {
const csp = cspOl(s);
console.log(nom, "→ o'ziga:",
formaYuboriladimi(csp, sahifa, sahifa),
"| begonaga:",
formaYuboriladimi(csp, sahifa, "https://yovuz.example/x"));
}Konsolda:
API → o'ziga: false | begonaga: false
obuna → o'ziga: true | begonaga: falseAPI sarlavhasi bilan forma umuman yuborilmaydi — 29-qismdagi xato. Obuna sahifasi sarlavhasi bilan — faqat o'ziga. Begona saytga yuborish ikkala holatda ham to'silgan. Model CSP'ning faqat bitta qoidasini tekshiradi; haqiqiy brauzer ko'proq qoidalarni (yo'naltirishlar, * manbalar) hisobga oladi.
10. Loyiha qadami: IntegratsiyaModule
Bu — 29-qism integratsiyalarini ko'chirishning uchinchi qismi. Commit — keyingi darsda.
- Yangi fayl —
src/integratsiya.module.ts:IntegratsiyaSozlamasi,yoqilganlar(),IntegratsiyaModule.forRoot(sozlama, configRejimi),XabarchiTinglovchi,IntegratsiyaYopuvchi— «IntegratsiyaModule.forRoot» bo'limidagi naqsh. Har xizmat (fayllar, bildirishnoma + push + SMS, obuna, Telegram ulash, webhook obunalari, jonli SSE, navbat) — uchta yo'l: test qiymati, config fabrikasi yokinull. - 29-qismdagi xizmat modullari o'zgarishsiz ko'chadi:
src/email/(pochta.ts,obuna.ts),src/bildirishnoma/,src/push/,src/sms/,src/telegram/,src/webhook/,src/realtime/(sse.ts,jonli.ts). - Yangi controller'lar — 29-qism routerlarining Nest shakli:
BildirishnomalarController,PushController,SmsController,ObunaController(CSP bilan),TelegramController,WebhooklarController. SMS callback va obuna —@Ochiq(), qolgani — global guard ostida. src/auth/auth.module.ts—pochtaProvider(«Token va async fabrika» bo'limidagi).src/app.module.tsdaIntegratsiyaModule.forRoot(...)ulanadi;src/ilova.tsda —/v1/obunava/v1/smsuchunexpress.urlencoded(pochta provayderi va SMS callback forma yuboradi)..env.examplega integratsiya o'zgaruvchilari:S3_*,SMTP_*,REDIS_URL,TELEGRAM_BOT_NOMI,VAPID_*,ISHCHI_DATABASE_URL— faqat namuna, haqiqiy kalit yo'q.
Tekshiruv:
npm run tip
npm run lint
npm testEndi 29-qism testlari (fayl, pochta, bildirishnoma, telegram, webhook, navbat, outbox) ishga tushadi — MinIO, Mailpit va Redis bo'lmasa, ular SKIP. Yakuniy jonli tekshiruv va commit — keyingi darsda. Hali commit qilmang.
11. Real ishda
- "Har tashqi xizmat — o'z moduli" — katta Nest loyihalarida
S3Module,MailModule,RedisModulealohida, har biriforRootAsyncbilan. Kanon ularni bittaIntegratsiyaModulega yig'di — 29-qismdagiserver.tstuzilmasiga yaqin bo'lsin deb. Ikkalasi ham to'g'ri; ko'p jamoali loyihada alohida modullar qulayroq. - Health check — fabrikada tekshirilgan ulanishlar
/health/readyda ham tekshiriladi (Health check'lar): ishga tushgandan keyin ham Redis uzilishi mumkin. - Intervyu: "Nest'da Redis klientini qanday ulaysiz?", "
forRootvaforRootAsyncfarqi?", "Ilova ishga tushganda tashqi xizmat yo'q bo'lsa nima qilasiz?".
Xulosa
- Tashqi klient — provider: token +
useFactory(async) +inject: [CONFIG]; fabrikada ulanish tekshiriladi (fail-fast). abortOnErrorsukut —true(exit 1);falsebilan xato Promise orqali chiqadi va o'zingiz boshqarasiz.- Controller'lar ro'yxati sinxron (
yoqilganlar(config)), bog'liqliklar — async fabrika; testlar tayyor qiymat beradi. - Yopish —
beforeApplicationShutdown(SSE, subscriber), keyinonApplicationShutdown(navbat). @nestjs/axios, mailer va Telegram modullari — standart, lekin kanondafetch, nodemailer va grammY to'g'ridan.- 29-qismdan qolgan CSP xatosi: obuna sahifasiga alohida
form-action 'self'— tuzatildi, brauzerda sinalmagan.
Keyingi dars: WebSocket gateway'lar — Nest gateway'ini to'liq yozamiz, SSE bilan o'lchab solishtiramiz va integratsiyalar ko'chishini commit qilamiz.
Manbalar
- NestJS hujjati — Dynamic modules, async providers: https://docs.nestjs.com/fundamentals/dynamic-modules
- NestJS hujjati — HTTP module (
@nestjs/axios): https://docs.nestjs.com/techniques/http-module - NestJS hujjati — Lifecycle events: https://docs.nestjs.com/fundamentals/lifecycle-events
- MDN — CSP
form-action: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action - Kanon (IlmHamroh, 2026-11-01):
src/integratsiya.module.ts,pochtaProvider,ObunaControllerCSP va testi, telegram-bot testi topilmasi. Fail-fast demo modul vaabortOnErrorchiqishi — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!