IlmHamroh
JavaScript Full-stack/30-qism. NestJS64/65-dars20 daqiqa
Mundarija (29)

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, SMTP verify(), Redis connect()) — xato bo'lsa ilova umuman ishga tushmaydi (fail-fast). Yopish — onApplicationShutdown hook'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, lekin vazifalar-api ularsiz — Node fetch, nodemailer va grammY to'g'ridan.

Bu darsda

  • Tashqi klientni token + async fabrika bilan provider qilasiz va inject bilan config'ni olasiz.
  • Fail-fast nima ekanini va abortOnError qanday 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:

  1. Qachon yaratiladi? Ilova ishga tushganda, bir marta.
  2. Sozlama qayerdan? Config'dan (S3_BUCKET, SMTP_HOST …).
  3. Ulanib bo'lmasa? Ilova ishga tushmasin — birinchi foydalanuvchi xatoni ko'rishidan oldin.
  4. Qanday yopiladi? SIGTERM'da ulanishlar to'g'ri yopilsin.
  5. 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):

ts
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 — Symbol token. Klass emas, chunki Pochta | 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'ning verify(): 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):

ts
@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:

text
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.create xato bilan yiqildi, port ochilmadi. Yopuvchi ning onApplicationShutdown i ham chaqirilmadi — ilova hech qachon "ishga tushmagan".

abortOnError sukut qiymatida (true) xuddi shu "yomon" holat:

text
[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 1

Jarayon 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:

  1. Sinxron: config qiymatlariga qarab qaysi xizmat yoqilgan — yoqilganlar(config). Shu ro'yxatdan controller'lar.
  2. 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):

js
// 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:

text
xotira (DEV): (yo'q)
postgres, Redis'siz: IlovalarController, FayllarController, OqimController
postgres + Redis: IlovalarController, FayllarController, MuddatController, OqimController

Tekshirib 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:

ts
@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):

ts
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

ts
@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

ts
@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):

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:

text
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.env dan darhol o'qiladi; xato bo'lsa darhol ConfigXatosi.
  • 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
js
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:

text
tayyor: redis, s3
  yopildi: s3
  yopildi: redis
ishga tushmadi: smtp — ECONNREFUSED

Muhim 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
js
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:

text
API → o'ziga: false | begonaga: false
obuna → o'ziga: true | begonaga: false

API 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.

  1. 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 yoki null.
  2. 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).
  3. 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.
  4. src/auth/auth.module.ts — pochtaProvider («Token va async fabrika» bo'limidagi).
  5. src/app.module.ts da IntegratsiyaModule.forRoot(...) ulanadi; src/ilova.ts da — /v1/obuna va /v1/sms uchun express.urlencoded (pochta provayderi va SMS callback forma yuboradi).
  6. .env.example ga integratsiya o'zgaruvchilari: S3_*, SMTP_*, REDIS_URL, TELEGRAM_BOT_NOMI, VAPID_*, ISHCHI_DATABASE_URL — faqat namuna, haqiqiy kalit yo'q.

Tekshiruv:

bash
npm run tip
npm run lint
npm test

Endi 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, RedisModule alohida, har biri forRootAsync bilan. Kanon ularni bitta IntegratsiyaModule ga yig'di — 29-qismdagi server.ts tuzilmasiga yaqin bo'lsin deb. Ikkalasi ham to'g'ri; ko'p jamoali loyihada alohida modullar qulayroq.
  • Health check — fabrikada tekshirilgan ulanishlar /health/ready da ham tekshiriladi (Health check'lar): ishga tushgandan keyin ham Redis uzilishi mumkin.
  • Intervyu: "Nest'da Redis klientini qanday ulaysiz?", "forRoot va forRootAsync farqi?", "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).
  • abortOnError sukut — true (exit 1); false bilan 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), keyin onApplicationShutdown (navbat).
  • @nestjs/axios, mailer va Telegram modullari — standart, lekin kanonda fetch, 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Tashqi xizmatlarni Nest modullariga o'rash: dinamik modul, async fabrika va fail-fast — IlmHamroh