IlmHamroh
JavaScript Full-stack/30-qism. NestJS12/65-dars21 daqiqa
Mundarija (30)

NestJS dynamic modullar: forRoot, forRootAsync va ConfigurableModuleBuilder

Qisqacha: Dinamik modul — tarkibi (provider'lari, importlari) ish vaqtida, sozlama bo'yicha hosil bo'ladigan modul. U @Module({...}) o'rniga statik metod qaytargan DynamicModule obyekti: { module, providers, imports, exports, global }. Konventsiya: forRoot(sozlama) — butun ilovaga bir marta, forRootAsync({ imports, inject, useFactory }) — sozlama boshqa provider'dan, register/forFeature — har iste'molchiga alohida. Bu metodlarni qo'lda yozish yoki ConfigurableModuleBuilder bilan avtomatik yasash mumkin.

Bu darsda

  • DynamicModule obyekti nimadan iboratini va statik moduldan farqini bilasiz.
  • forRoot, forRootAsync, register, forFeature konventsiyalarini farqlaysiz.
  • ConfigurableModuleBuilder nima yasashini kanon tajribasida ko'rasiz.
  • Modul daraxti dinamik modullar bilan qanday yig'ilishini JS modelida takrorlaysiz.
  • vazifalar-api ning OmborModule iga ikki yo'l berasiz: tayyor ombor (testlar) yoki config bo'yicha fabrika.

Oldin bilishingiz kerak: Circular dependency, ModuleRef va lazy modullar, Custom provider'lar, Modullar, Getter/setter va static a'zolar.

O'tgan darsda halqalardan qochishni gaplashdik. Endi oldingi uch darsning bo'laklari birlashadi: Custom provider'lar darsida yozilgan ombor fabrikasi hali ulanmagan edi. Bu darsda u ulanadi — dinamik modul orqali.

1. Nega bu kerak?

Custom provider'lar darsidagi OmborModule oddiy, statik modul edi: @Module({ providers: [...] }). Uning tarkibi kodda qotgan — har doim config bo'yicha fabrika.

Lekin bizga ikki xil xulq kerak:

  • Prodakshn (main.ts): ombor config bo'yicha, Nest ichida yaratilsin.
  • Testlar: test tayyor XotiraOmbori yoki ataylab "buzuq" omborni beradi va Nest aynan shuni ishlatsin.

Statik modulda buni qilib bo'lmaydi. Bitta @Module — bitta tarkib. Kerak bo'lgani — modulni qaytaradigan funksiya: "menga tayyor ombor ber" yoki "config'dan yasa" deb chaqiriladi va mos tarkibni qaytaradi.

O'xshatish: mebel do'konida tayyor shkaf (statik modul) va buyurtma shkaf (dinamik modul). Buyurtmada siz o'lcham va rang berasiz, usta shunga mos shkaf yig'adi. Model bitta, natija — buyurtmaga qarab.

1.1 Express'da qanday edi

25–27-qismlarda bu muammo bor edi va biz uni oddiy funksiya bilan hal qilgandik. ilovaYarat({ ombor }) — omborni argument sifatida oladi. main.ts omborni config bo'yicha o'zi yaratib, shu funksiyaga berardi. Testlar esa new XotiraOmbori() ni berardi. Ya'ni "kim yaratadi" degan qaror chaqiruvchida edi.

Nest'da ilova qo'lda yig'ilmaydi — uni konteyner modullardan yig'adi. Shuning uchun tanlov ham modul darajasiga ko'chadi. Dinamik modul — aynan o'sha ilovaYarat({ ombor }) g'oyasi, faqat modul shaklida: argument oladi va shunga mos tarkib qaytaradi.

Express (25–27) Nest (shu dars)
Tanlov qayerda ilovaYarat argumenti OmborModule.forRoot yoki forRootAsync
Ombor kim yaratadi main.ts yoki test test yoki Nest fabrikasi
Yutuq sodda, ko'rinib turadi konteyner ulaydi, yopishni ham o'z zimmasiga oladi
Narx qo'lda uzatish bitta qo'shimcha tushuncha — dinamik modul

2. DynamicModule obyekti

2.1 Tuzilishi

Siz buni allaqachon ko'rgansiz — BogliqliklarModule.forRoot(...) Provider'lar darsidan beri loyihada:

ts
@Global()
@Module({})
export class BogliqliklarModule {
  static forRoot(b: TashqiBogliqliklar): DynamicModule {
    return {
      module: BogliqliklarModule,
      providers: [/* … */],
      exports: [/* … */],
    };
  }
}

DynamicModule — oddiy obyekt:

Maydon Ma'nosi
module qaysi klass — majburiy; modulning "ismi"
providers, exports, imports, controllers @Module dagidek
global true — @Global() bilan bir xil

Bu maydonlar klassning @Module({...}) dagi tarkibiga qo'shiladi. Shuning uchun dekoratordagi @Module({}) bo'sh — hamma narsa metoddan keladi. static — metod klassning o'ziga tegishli, nusxaga emas (static a'zolar): OmborModule.forRoot(...) — new siz.

2.2 Kanon tajribasidagi obyekt

Kanon tajribasi tayyor forRoot natijasini ekranga chiqargan. Qatordagi to'rt qiymatga qarang — ular jadvaldagi maydonlar:

text
DynamicModule obyekti (forRoot natijasi): module=XabarModule, global=true, providers=1, exports=0

Bu — ConfigurableModuleBuilder yasagan forRoot ning natijasi (pastda batafsil). Obyekt oddiy: modul nomi, global belgisi, bitta provider (sozlamalar). Nest uni imports ichida ko'rganda, oddiy modul klassi kabi daraxtga qo'shadi.

exports=0 ga e'tibor bering. Tajribadagi modul o'z sozlamasini tashqariga bermaydi — uni faqat ichidagi xizmat ishlatadi. Bu to'g'ri: sozlama — modulning ichki ishi.

2.3 O'zingiz to'ldiring

Quyidagi forRoot tayyor ombor oladi va uni hamma modulga ko'rinadigan qiladi. Bo'sh joylarni to'ldiring:

  • Qaytariladigan obyektdagi majburiy maydon: : OmborModule
  • Provider: { provide: VAZIFA_OMBORI, : ombor }
  • Boshqa modullar ko'rishi uchun: : [VAZIFA_OMBORI]
  • Import qilmasdan ham ko'rinsin: global:

Bu — loyiha qadamidagi OmborModule.forRoot ning deyarli o'zi. Farqi bitta: u yerda global o'rniga klassdagi @Global() dekoratori ishlatilgan — natija bir xil.

3. Konventsiyalar

Metod nomlari Nest ekotizimida kelishilgan — nomga qarab ma'nosini bilasiz:

Metod Ma'nosi Misol
forRoot(sozlama) butun ilovaga bir marta, ildizda TypeOrmModule.forRoot({...})
forRootAsync({...}) xuddi shu, sozlama boshqa provider'dan JwtModule.registerAsync, TypeOrmModule.forRootAsync
forFeature(...) har feature modulga o'z qismi TypeOrmModule.forFeature([Vazifa])
register(sozlama) har iste'molchiga alohida nusxa HttpModule.register({ timeout })

forRoot + forFeature juftligi ko'p uchraydi: ildizda baza ulanishi bir marta (forRoot), har feature modul o'z jadvallari uchun ombor oladi (forFeature). Bu naqshni TypeORM bilan NestJS darsida ko'rasiz.

forRootAsync ning tipik shakli — o'tgan darslardagi fabrika:

ts
XabarModule.forRootAsync({
  imports: [SozlamaModule],
  inject: [Sozlamalar],
  useFactory: async (s: Sozlamalar) => ({
    prefiks: s.prefiks(),
    daraja: "warn" as const,
  }),
})

imports — fabrika uchun kerak provider qaysi moduldan; inject — fabrika argumentlari; useFactory — sozlamani qaytaradi (async ham mumkin).

3.1 forRootAsync ning uch shakli

useFactory — eng ko'p uchraydigan shakl, lekin yagona emas. Nest kutubxonalarining forRootAsync (yoki registerAsync) metodi odatda uch shaklni qabul qiladi:

Shakl Ma'nosi Qachon
useFactory + inject sozlamani funksiya qaytaradi ko'p holatda — eng sodda
useClass Nest klass nusxasini yaratadi, sozlamani uning metodi beradi sozlama mantiqi katta, o'z bog'liqliklari bor
useExisting xuddi shunday, lekin mavjud provider'ni oladi sozlama klassi boshqa joyda allaqachon bor

useClass shaklida klass maxsus metodni bajaradi. ConfigurableModuleBuilder yasagan modulda bu metodning sukut nomi — create(); u sozlama obyektini qaytaradi. Tashqi kutubxonalarda nom boshqacha bo'lishi mumkin (masalan, createTypeOrmOptions()) — hujjatiga qarang.

Bu uch shaklning mohiyati bitta: sozlamani qachon va kim hisoblashini konteynerga topshirish. Sizga kerak provider'lar (config, log) tayyor bo'lgach, Nest fabrikani yoki klass metodini chaqiradi.

3.2 Bir modul — bir necha nusxa

forRoot va register farqi qog'ozda kichik ko'rinadi. Amalda u "ilovada nechta ulanish ochiladi" degan savolga javob beradi. Nest dinamik modulni klassi va tarkibi bo'yicha taniydi. Tarkib bir xil bo'lsa — bitta nusxa; boshqacha bo'lsa — alohida nusxa.

Kichik modelda ko'raylik. Konteyner modul nusxalarini kalit bo'yicha saqlaydi: klass nomi + sozlama. Brauzerda ishlaydi:

js
const nusxalar = new Map();
let ulanishlar = 0;

// Nest'ning modul kaliti g'oyasi: klass + tarkib
function yukla(dinamik) {
  const kalit = dinamik.module.name +
    JSON.stringify(dinamik.sozlama);
  if (!nusxalar.has(kalit)) {
    ulanishlar++;
    nusxalar.set(kalit, `ulanish #${ulanishlar}`);
  }
  return nusxalar.get(kalit);
}

class HttpModule {
  static register(sozlama) {
    return { module: HttpModule, sozlama };
  }
}

// ikki feature modul, har biri o'z timeout'i bilan
console.log("vazifalar:",
  yukla(HttpModule.register({ timeout: 3000 })));
console.log("eksport:",
  yukla(HttpModule.register({ timeout: 30000 })));
// uchinchisi birinchisi bilan bir xil sozlama
console.log("teglar:",
  yukla(HttpModule.register({ timeout: 3000 })));
console.log("jami ulanish:", ulanishlar);

Konsolda:

text
vazifalar: ulanish #1
eksport: ulanish #2
teglar: ulanish #1
jami ulanish: 2

Nima bo'ldi? Birinchi ikki modul turli sozlama berdi — ikki alohida nusxa. Uchinchi modul (teglar) birinchisi bilan aynan bir xil sozlama berdi va o'sha nusxani oldi. Bu register uchun bu to'g'ri xulq: har iste'molchi o'ziga mos HTTP klientini oladi.

Endi baza ulanishini tasavvur qiling. Har feature modul DatabaseModule.forRoot({ url }) ni o'zi chaqirsa va sozlamalar biroz farq qilsa (masalan, biri max: 10, boshqasi max: 20) — ikki pool ochiladi. Postgres ulanishlari ikki baravar ko'payadi va hech qanday xato chiqmaydi. Shuning uchun forRoot qoidasi qat'iy: ildizda, bir marta. Feature modullar esa tayyor provider'ni oladi.

Model soddalashtirilgan: haqiqiy Nest kalitni JSON.stringify bilan emas, modul metama'lumotidan hisoblaydi. G'oya esa aynan shu.

Tekshirib ko'ring: VazifalarModule va EksportModule har biri RedisModule.forRoot({ url: REDIS_URL }) ni import qildi, ikkalasida URL bir xil. Ikki ulanish ochiladimi?

Javob

Bizning modelda — yo'q: kalit bir xil, nusxa bitta. Lekin bunga tayanmang. Ertaga kimdir bitta modulda { url, db: 1 } yozsa — kalit o'zgaradi va jim ikkinchi ulanish ochiladi. Ishonchli yo'l — forRoot ni AppModule da bir marta chaqirish va provider'ni eksport qilish (yoki modulni global qilish).

4. ConfigurableModuleBuilder

4.1 Avtomatik forRoot va forRootAsync

forRoot va forRootAsync ni har modulda qo'lda yozish takroriy ish. Nest buning uchun quruvchi beradi:

ts
type XabarSozlamasi = { prefiks: string; daraja?: "info" | "warn" };

const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<XabarSozlamasi>()
    .setClassMethodName("forRoot")
    .setExtras(
      { isGlobal: false },
      (def, extras) => ({ ...def, global: extras.isGlobal }),
    )
    .build();

@Injectable()
class Xabarchi {
  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private readonly s: XabarSozlamasi,
  ) {}
  yoz(x: string) {
    return `[${this.s.prefiks}:${this.s.daraja ?? "info"}] ${x}`;
  }
}

@Module({ providers: [Xabarchi], exports: [Xabarchi] })
class XabarModule extends ConfigurableModuleClass {}

Qatorma-qator:

  • new ConfigurableModuleBuilder<XabarSozlamasi>() — sozlama turi bilan quruvchi.
  • .setClassMethodName("forRoot") — metod nomi (sukut — register). Natijada forRoot va forRootAsync paydo bo'ladi.
  • .setExtras({ isGlobal: false }, …) — sozlamaga qo'shimcha maydon (isGlobal), u modulning global maydoniga aylanadi.
  • MODULE_OPTIONS_TOKEN — sozlamalar token'i; xizmat uni @Inject bilan oladi.
  • extends ConfigurableModuleClass — modul klassi tayyor statik metodlarni meros oladi.

Kanon natijasi (Vitest muhitida):

text
forRoot: [sinxron:info] salom
forRootAsync (imports + inject): [vazifalar:warn] salom
OPTIONS_TYPE/ASYNC_OPTIONS_TYPE — faqat tur uchun (ish vaqtida qiymati): object, object

forRoot({ prefiks: "sinxron" }) — sozlama to'g'ridan. forRootAsync({...}) — sozlama boshqa moduldagi provider'dan, fabrika orqali. OPTIONS_TYPE va ASYNC_OPTIONS_TYPE — faqat TypeScript turlari uchun (typeof XabarModule.OPTIONS_TYPE); ish vaqtida ular ma'nosiz bo'sh obyekt.

4.2 Qo'lda yoki quruvchi bilan

vazifalar-api da kanon OmborModule ni qo'lda yozdi. Sabab — o'qish oson: ikki metod, har biri nima qaytarishi ko'rinib turibdi. Quruvchi esa kuchli, lekin sehrli: forRootAsync qayerdan kelgani, useClass va useExisting variantlari ham borligi kodda ko'rinmaydi.

Qoida: modul bir-ikki joyda ishlatiladigan bo'lsa — qo'lda. Boshqa jamoalar foydalanadigan kutubxona moduli bo'lsa — quruvchi: u useFactory, useClass, useExisting shakllarining hammasini bepul beradi va standart ko'rinishda.

Tekshirib ko'ring: Hamkasbingiz quruvchida .setClassMethodName("forRoot") ni yozishni unutdi. AppModule da esa XabarModule.forRoot({ prefiks: "x" }) chaqirilgan. TypeScript nima deydi va qaysi metodlar bor?

Javob

TypeScript xato beradi: forRoot degan statik metod yo'q. Sukut nom — register, demak quruvchi register va registerAsync ni yasagan. Yo chaqiruvni XabarModule.register(...) ga o'zgartiring, yo quruvchiga setClassMethodName("forRoot") qo'shing. Nomni ma'noga qarab tanlang: butun ilovaga bir marta bo'lsa — forRoot, har iste'molchiga alohida bo'lsa — register.

4.3 O'z forRoot ingiz: to'rt savol

Qo'lda yozasizmi yoki quruvchi bilanmi — har dinamik modul uchun to'rt savolga javob bering. Ular keyingi bo'limlardagi xatolarning deyarli hammasini oldindan yopadi.

  1. Sozlama to'g'rimi? forRoot istalgan obyektni qabul qiladi. Bo'sh prefiks yoki yo'q URL bilan ilova ishga tushsa, xato birinchi so'rovda chiqadi — ancha kech. Metod boshida tekshiring va aniq xabar bilan xato tashlang.
  2. Sukut qiymatlar qayerda? { daraja: "info", ...sozlama } — bitta joyda. Ular xizmat ichiga tarqalib ketsa, "sukut nima edi?" degan savolga javob topish qiyin.
  3. Nima eksport qilinadi? Provider modul ichida yaratiladi, lekin exports da bo'lmasa — boshqa modullar uni ko'rmaydi. Faqat tashqariga kerak narsani eksport qiling; ichki yordamchilar (masalan, SAQLASH) yashirin qolsin.
  4. Global bo'lsinmi? global: true qulay: har feature modul import qilmasdan oladi. Lekin "bu provider qayerdan keldi?" degan savol qiyinlashadi. Global — butun ilovaga kerak, bitta nusxali narsalar uchun (config, log, ombor).

vazifalar-api dagi OmborModule shu savollarga shunday javob beradi: tekshiruv — AppModule.forRoot da, sukut yo'q (ombor yoki config shart), eksport — faqat VAZIFA_OMBORI, global — ha.

5. JS modelida dinamik modul

Nest daraxtni yig'ganda klass ham, DynamicModule obyekti ham bir xil ko'rinishga keltiriladi. Buni kichik modelda takrorlaymiz — brauzerda ishlaydi:

js
// Modul — klass (static meta) yoki { module, providers, imports }
const tarkib = (m) =>
  typeof m === "function" ? { module: m, ...m.meta } : m;

function yigish(ildiz, natija = []) {
  const t = tarkib(ildiz);
  for (const i of t.imports ?? []) yigish(i, natija);
  const p = (t.providers ?? []).join(", ") || "—";
  natija.push(`${t.module.name}: ${p}`);
  return natija;
}

class OmborModule {
  static forRoot(ombor) {
    return { module: OmborModule, providers: [`OMBOR=${ombor}`] };
  }
  static forRootAsync() {
    return {
      module: OmborModule,
      providers: ["SAQLASH (fabrika)", "OMBOR=SAQLASH.ombor"],
    };
  }
}
class VazifalarModule {
  static meta = { providers: ["VazifalarService"] };
}
class AppModule {
  static forRoot({ ombor }) {
    return {
      module: AppModule,
      imports: [
        ombor
          ? OmborModule.forRoot(ombor)
          : OmborModule.forRootAsync(),
        VazifalarModule,
      ],
    };
  }
}

const tayyor = AppModule.forRoot({ ombor: "XotiraOmbori" });
console.log(yigish(tayyor).join("\n"));
console.log("---");
console.log(yigish(AppModule.forRoot({})).join("\n"));

Konsolda:

text
OmborModule: OMBOR=XotiraOmbori
VazifalarModule: VazifalarService
AppModule: —
---
OmborModule: SAQLASH (fabrika), OMBOR=SAQLASH.ombor
VazifalarModule: VazifalarService
AppModule: —

Bitta AppModule.forRoot — ikki xil daraxt. Birinchisida ombor tayyor (useValue), ikkinchisida fabrika. tarkib funksiyasi — Nest ichidagi ishning qisqa shakli: klass bo'lsa metama'lumotdan, obyekt bo'lsa maydonlardan. Model soddalashtirilgan: haqiqiy Nest dekoratordagi va obyektdagi maydonlarni birlashtiradi, bir modulni ikki marta yig'maydi va global modullarni alohida hisobga oladi.

flowchart TB
  A["AppModule.forRoot(b)"] --> B["BogliqliklarModule.forRoot: log, xabarchi, sozlamalar, config"]
  A --> C{"b.ombor bormi?"}
  C -- ha --> D["OmborModule.forRoot(ombor): useValue"]
  C -- yo'q --> E["OmborModule.forRootAsync(): fabrika"]
  A --> V["VazifalarModule"]
  A --> X["EksportModule"]

Diagramma loyiha qadamidagi tanlovni ko'rsatadi: testlar yuqori tarmoqdan (tayyor ombor), main.ts keyinroq pastki tarmoqdan (fabrika) o'tadi.

Tekshirib ko'ring: OmborModule.forRoot(a) va OmborModule.forRoot(b) ikki xil feature modulda import qilinsa, ilovada nechta OmborModule bo'ladi?

Javob

Ikkita. Nest dinamik modulni module klassi va uning tarkibi (provider'lar) bo'yicha ajratadi; har xil tarkibli ikki forRoot — ikki alohida modul nusxasi. Shuning uchun forRoot ildizda bir marta chaqiriladi, feature modullar esa u eksport qilgan provider'ni (bizda — global VAZIFA_OMBORI) oladi. Har feature'ga alohida nusxa kerak bo'lsa — register/forFeature naqshi.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
forRoot ichidagi provider boshqa modulda ko'rinmaydi exports yoki global unutilgan exports: [TOKEN], kerak bo'lsa global: true
Ikki ulanish, ikki pool forRoot bir necha feature modulda chaqirilgan ildizda bir marta
module maydoni yo'q DynamicModule noto'g'ri obyekt module: KlassNomi majburiy
forRootAsync fabrikasi provider'ni topmaydi fabrika uchun imports berilmagan imports: [SozlamaModule]
Sozlama tekshirilmay qoladi forRoot har qanday obyektni qabul qiladi metod boshida tekshiruv va aniq xato

Ikkitasini batafsil ko'raylik — ular eng ko'p vaqt oladi.

Provider ko'rinmaydi. Nest bu holatda DI va IoC konteyner darsidagi tanish xatoni beradi: Nest can't resolve dependencies of the …. Xabar "provider yo'q" deydi, lekin aslida provider bor — u boshqa modul ichida yopiq qolgan. Birinchi tekshiradigan joy: dinamik modulning exports maydoni. Ikkinchisi: iste'molchi modul uni imports ga qo'shganmi yoki modul globalmi.

Ikki pool. Bu xato hech qanday xabar bermaydi. Ilova ishlaydi, testlar o'tadi. Faqat bazada ulanishlar soni kutilganidan ko'p bo'ladi, keyinroq esa "too many connections" chiqadi. Topish usuli — Lifecycle hook'lar darsida ko'radigan pg_stat_activity so'rovi: unda bitta ilovadan nechta seans borligi ko'rinadi.

7. Mashqlar

Mashqlar kurs/mashqlar/30/12-dinamik-modul/ da.

1-mashq (oson): qaysi metod?

  • Baza ulanishi — butun ilovaga bir marta:
  • Xuddi shu, lekin URL ConfigService dan:
  • Har feature modulga o'z jadvallari:
  • DynamicModule dagi majburiy maydon:
  • Quruvchi yaratadigan sozlamalar tokeni:
Yechim
  • forRoot — ildizda bir marta.
  • forRootAsync — inject: [ConfigService], useFactory.
  • forFeature — feature modul darajasida.
  • module — qaysi klass.
  • MODULE_OPTIONS_TOKEN — xizmat sozlamani shu token bilan oladi.

2-mashq (o'rta): sozlamani tekshiradigan forRoot

XabarModule.forRoot(sozlama) ni JS'da yozing. U sukut qiymatlarni qo'shsin (daraja: "info"), prefiks bo'sh satr yoki yo'q bo'lsa Error("XabarModule.forRoot: prefiks kerak") tashlasin va { module, global, providers: [{ provide: "XABAR_SOZLAMASI", useValue }] } qaytarsin. global — sozlama.isGlobal ?? false. Uch holatni sinang. Ishora: spread bilan sukut qiymatlar — { daraja: "info", ...sozlama }.

Yechim
js
class XabarModule {
  static forRoot(sozlama = {}) {
    const { isGlobal = false, ...qolgan } = sozlama;
    const toliq = { daraja: "info", ...qolgan };
    if (!toliq.prefiks) {
      throw new Error("XabarModule.forRoot: prefiks kerak");
    }
    return {
      module: XabarModule,
      global: isGlobal,
      providers: [{ provide: "XABAR_SOZLAMASI", useValue: toliq }],
    };
  }
}

const d1 = XabarModule.forRoot({ prefiks: "vazifalar" });
console.log(d1.global, JSON.stringify(d1.providers[0].useValue));
const d2 = XabarModule.forRoot({
  prefiks: "eksport", daraja: "warn", isGlobal: true,
});
console.log(d2.global, JSON.stringify(d2.providers[0].useValue));
try {
  XabarModule.forRoot({ daraja: "warn" });
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
false {"daraja":"info","prefiks":"vazifalar"}
true {"daraja":"warn","prefiks":"eksport"}
XabarModule.forRoot: prefiks kerak

isGlobal sozlamaga tushmadi — u modul darajasidagi belgi (quruvchidagi setExtras ham aynan shuni qiladi). Xato ilova yig'ilayotganda chiqadi — fail-fast. vazifalar-api dagi AppModule.forRoot ham shunday tekshiruv bilan boshlanadi.

3-mashq (qiyin): forRootAsync modeli

forRootAsync({ inject, useFactory }) qaytaradigan obyekt va uni "ishga tushiradigan" yukla(dinamikModul, tayyorProviderlar) funksiyasini yozing. yukla inject dagi tokenlarni tayyorProviderlar (Map) dan olib, useFactory ni await bilan chaqirsin va XABAR_SOZLAMASI qiymatini qaytarsin. Token topilmasa — Error("<token> topilmadi"). Sinov: CONFIG dan prefiks oladigan async fabrika, keyin CONFIG siz. Ishora: Promise.all, map.

Yechim
js
class XabarModule {
  static forRootAsync({ inject = [], useFactory }) {
    return {
      module: XabarModule,
      providers: [{ provide: "XABAR_SOZLAMASI", inject, useFactory }],
    };
  }
}

async function yukla(dinamik, tayyor) {
  const p = dinamik.providers[0];
  const args = p.inject.map((t) => {
    if (!tayyor.has(t)) throw new Error(`${String(t)} topilmadi`);
    return tayyor.get(t);
  });
  return p.useFactory(...await Promise.all(args));
}

const CONFIG = Symbol("CONFIG");
const modul = XabarModule.forRootAsync({
  inject: [CONFIG],
  useFactory: async (config) => ({
    prefiks: config.ILOVA, daraja: "warn",
  }),
});

const tayyor = new Map([[CONFIG, { ILOVA: "vazifalar" }]]);
console.log(await yukla(modul, tayyor));
try {
  await yukla(modul, new Map());
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
{ prefiks: 'vazifalar', daraja: 'warn' }
Symbol(CONFIG) topilmadi

forRootAsync o'zi hech narsa chaqirmaydi — u faqat retsept qaytaradi (inject + useFactory). Fabrika keyin, konteyner hamma provider'ni yig'ayotganda ishlaydi. vazifalar-api dagi OmborModule.forRootAsync() ham shunday: chaqirilganda baza ochilmaydi, faqat ilova yaratilayotganda.

8. Loyiha qadami: OmborModule.forRoot va forRootAsync

8.1 Reja

Custom provider'lar darsidagi OmborModule dinamik modulga aylanadi va daraxtga ulanadi. AppModule.forRoot qaror qiladi: tayyor ombor berilgan bo'lsa — forRoot(ombor), aks holda — forRootAsync(). BogliqliklarModule dan ombor chiqariladi: endi uning yagona egasi — OmborModule.

Nega ombor BogliqliklarModule dan chiqadi? Qoida: har resursning bitta egasi bo'lsin. Ombor ikki modulda provider bo'lsa, qaysi biri ishlatilishi import tartibiga bog'liq bo'lib qoladi — bunday xatoni topish qiyin. Endi VAZIFA_OMBORI faqat OmborModule da. Kelajakda ombor ulanishlarini yopish ham o'sha yerda bo'ladi.

main.ts bu darsda o'zgarmaydi: u hali omborni o'zi yaratib, tayyor beradi (forRoot yo'li). Fabrika yo'liga o'tish ombor ulanishlarini yopishni talab qiladi — u Lifecycle hook'lar darsida.

8.2 ombor.module.ts

ts
// ombor.module.ts — dinamik modul: VAZIFA_OMBORI provider'ini ikki
// xil beradi.
//   forRoot(ombor) — tayyor obyekt (testlar: XotiraOmbori, "buzuq"
//     ombor) — useValue, yopish chaqiruvchining ishi;
//   forRootAsync() — config bo'yicha (SAQLASH=xotira|fayl|postgres|
//     prisma, REDIS_URL): ASYNC fabrika provider — Nest bootstrap
//     paytida Promise'ni kutadi (baza ulanishi, migratsiya
//     tekshiruvi — fail-fast)
import { Global, Module } from "@nestjs/common";
import type { DynamicModule } from "@nestjs/common";
import { omborYarat } from "./ombor-yarat.ts";
import type { Saqlash } from "./ombor-yarat.ts";
import type { Config } from "./config.ts";
import type { Log } from "./umumiy/log.ts";
import { CONFIG, LOG, VAZIFA_OMBORI } from "./tokenlar.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";

const SAQLASH = Symbol("SAQLASH");

@Global()
@Module({})
export class OmborModule {
  static forRoot(ombor: VazifaOmbori): DynamicModule {
    return {
      module: OmborModule,
      providers: [{ provide: VAZIFA_OMBORI, useValue: ombor }],
      exports: [VAZIFA_OMBORI],
    };
  }

  static forRootAsync(): DynamicModule {
    return {
      module: OmborModule,
      providers: [
        // async fabrika: Nest Promise'ni kutadi; xato bo'lsa —
        // ilova ishga TUSHMAYDI (EGASI_ID yo'q, baza yo'q,
        // migratsiya orqada)
        {
          provide: SAQLASH,
          useFactory: (config: Config, log: Log): Promise<Saqlash> =>
            omborYarat(config, log),
          inject: [CONFIG, LOG],
        },
        {
          provide: VAZIFA_OMBORI,
          useFactory: (s: Saqlash) => s.ombor,
          inject: [SAQLASH],
        },
      ],
      exports: [VAZIFA_OMBORI],
    };
  }
}

Dekoratordagi @Module({}) endi bo'sh, provider'lar metodlarda. Birinchi metod — forRoot — bitta useValue qo'shadi. Uning ma'nosi: "bu omborni beraman, yaratish va yopish meniki emas". Ikkinchisi — forRootAsync — o'tgan darsdagi fabrikalar aynan, faqat metod ichida. forRootAsync argument olmaydi: unga kerak hamma narsa (CONFIG, LOG) global BogliqliklarModule dan keladi. @Global() klassda qoldi — ikkala variant ham global.

8.3 app.module.ts

ts
// app.module.ts — ildiz modul: ilova shu moduldan boshlab yig'iladi
// (imports daraxti). forRoot(...) — dinamik modul: ombor tayyor
// berilsa (testlar) — shu, aks holda config bo'yicha
// OmborModule.forRootAsync() yaratadi
import { Module } from "@nestjs/common";
import type { DynamicModule } from "@nestjs/common";
import { BogliqliklarModule } from "./bogliqliklar.module.ts";
import type { TashqiBogliqliklar } from "./bogliqliklar.module.ts";
import { OmborModule } from "./ombor.module.ts";
import { VazifalarModule } from "./vazifalar/vazifalar.module.ts";
import { EksportModule } from "./eksport/eksport.module.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";

export type IlovaBogliqliklari = TashqiBogliqliklar & {
  ombor?: VazifaOmbori | undefined;
};

@Module({})
export class AppModule {
  static forRoot(
    { ombor, ...b }: IlovaBogliqliklari,
  ): DynamicModule {
    if (ombor === undefined && b.config === undefined) {
      throw new Error("AppModule.forRoot: ombor yoki config kerak");
    }
    return {
      module: AppModule,
      imports: [
        BogliqliklarModule.forRoot(b),
        ombor === undefined
          ? OmborModule.forRootAsync()
          : OmborModule.forRoot(ombor),
        VazifalarModule,
        EksportModule,
      ],
    };
  }
}

Uch narsa. { ombor, ...b } — ombor ajratiladi, qolgani BogliqliklarModule ga (rest). Tekshiruv: na ombor, na config — fabrika ishlay olmaydi, darhol aniq xato («Sozlamani tekshiradigan forRoot» mashqidagi naqsh). Va shartli import — modul daraxti endi sozlamaga bog'liq.

8.4 bogliqliklar.module.ts va ilova.ts

BogliqliklarModule dan ombor chiqariladi:

diff
-import {
-  CONFIG, ILOVA_SOZLAMALARI, LOG, VAZIFA_OMBORI, XABARCHI,
-} from "./tokenlar.ts";
+import {
+  CONFIG, ILOVA_SOZLAMALARI, LOG, XABARCHI,
+} from "./tokenlar.ts";
 import type { IlovaSozlamalari } from "./tokenlar.ts";
 import type { Config } from "./config.ts";
-import type { VazifaOmbori } from "./vazifalar/ombor.ts";
diff
 export type TashqiBogliqliklar = {
-  ombor: VazifaOmbori;
   log: Log;
diff
   static forRoot(
-    { ombor, log, xabarchi, sozlamalar, config }: TashqiBogliqliklar,
+    { log, xabarchi, sozlamalar, config }: TashqiBogliqliklar,
   ): DynamicModule {
     const providers: Provider[] = [
-      { provide: VAZIFA_OMBORI, useValue: ombor },
       { provide: LOG, useValue: log },

Fayl boshidagi izohdan ham "ombor" so'zi ketadi: endi u log, xabarchi va config uchun. ilova.ts da ombor ixtiyoriy bo'ladi:

diff
 export type Bogliqliklar = {
-  ombor: VazifaOmbori;
-  /** to'liq config (main.ts) — DI ichida CONFIG tokeni */
+  /** tayyor ombor (testlar); berilmasa — config bo'yicha
+   * OmborModule.forRootAsync() yaratadi va yopadi */
+  ombor?: VazifaOmbori;
+  /** to'liq config (main.ts) — ombor tanlash uchun */
   config?: Config;

ilovaYarat qolgani o'zgarmaydi: u { ombor, config, … } ni AppModule.forRoot ga uzatardi — endi ombor yo'q bo'lishi ham mumkin.

8.5 Test: ikkala yo'l

Yangi test/ombor-modul.test.ts — avval ikki test:

ts
// ombor-modul.test.ts — OmborModule.forRootAsync() (async fabrika
// provider, dinamik modul): config bo'yicha ombor DI ichida
// yaratiladi
import { test } from "vitest";
import assert from "node:assert/strict";
import { ilovaYarat } from "../src/ilova.ts";
import { configYukla } from "../src/config.ts";
import { VAZIFA_OMBORI } from "../src/tokenlar.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";

test("SAQLASH=xotira: ombor config'dan (fabrika) — XotiraOmbori, " +
  "HTTP ishlaydi", async () => {
    const app = await ilovaYarat({
      config: configYukla({ SAQLASH: "xotira" }),
    });
    assert.ok(app.get(VAZIFA_OMBORI) instanceof XotiraOmbori);
    await app.close();
  });

test("ombor ham, config ham yo'q — darhol xato", async () => {
  await assert.rejects(ilovaYarat({}), /ombor yoki config kerak/);
});

Testlar uchun alohida baza kerak emas: SAQLASH=xotira rejimi fabrika yo'lini xotirada sinaydi. Ikki test ikki savolga javob beradi. Fabrika yo'li umuman ishlaydimi? Va noto'g'ri chaqiruv aniq xato beradimi? Ikkinchi savol muhim: xato xabari keyingi dasturchiga nima qilish kerakligini aytadi.

Birinchi test fabrika yo'lini sinaydi: ombor berilmadi, config bor — forRootAsync ishladi. app.get(VAZIFA_OMBORI) — ilova konteyneridan provider'ni olish (ModuleRef.get ning ilova darajasidagi shakli). Ikkinchi test — AppModule.forRoot dagi tekshiruv. Qolgan 25–27 testlari esa tayyor XotiraOmbori beradi — ular forRoot(ombor) yo'lidan o'tadi va o'zgarmaydi.

8.6 Tekshiruv

bash
npm run tip
npm run lint
npm test

Biz kanon nusxasida yurgizdik: tip 0, lint 0; ombor-modul.test, supertest.test, api.test — 12/12 o'tdi. Hali commit qilmang — holat commit'i Reflector va DiscoveryService darsida.

9. Real ishda

  • Nest ekotizimi dinamik modullarga qurilgan: ConfigModule.forRoot, TypeOrmModule.forRootAsync, BullModule.registerQueue, JwtModule.register. Bu darsdan keyin ularning hujjatini o'qish ancha oson.
  • O'z kutubxonangiz (masalan, kompaniyadagi SMS yoki to'lov moduli) — ConfigurableModuleBuilder bilan; foydalanuvchilar standart forRoot/forRootAsync ni kutadi.
  • Testlarda dinamik modul almashtirish nuqtasi: prodakshnda fabrika, testda tayyor obyekt — vazifalar-api dagidek.
  • Kod review'da yangi forRoot ko'rsangiz, to'rt savolni bering: sozlama tekshiriladimi, sukutlar qayerda, nima eksport qilinadi, global bo'lishi kerakmi. Feature modul ichida forRoot chaqiruvi — darhol savol: "bu ikkinchi ulanish ochmaydimi?".
  • Intervyu: "Dynamic module nima?", "forRoot va forFeature farqi?", "forRootAsync qachon kerak?".

Xulosa

  • Dinamik modul — statik metod qaytargan DynamicModule obyekti; tarkibi sozlamaga bog'liq.
  • module majburiy; providers, imports, exports, global — @Module dagidek.
  • forRoot — ildizda bir marta, forRootAsync — sozlama provider'dan, forFeature/register — har iste'molchiga.
  • ConfigurableModuleBuilder forRoot/forRootAsync va MODULE_OPTIONS_TOKEN ni avtomatik yasaydi; kichik ichki modulda qo'lda yozish o'qish uchun osonroq.
  • Nest dinamik modulni klass va tarkib bo'yicha taniydi: turli sozlamali ikki forRoot — ikki nusxa, ya'ni ikki ulanish. Shuning uchun forRoot ildizda bir marta chaqiriladi.
  • forRootAsync uch shaklda keladi: useFactory, useClass, useExisting; hammasida sozlamani konteyner kerakli lahzada hisoblaydi.
  • vazifalar-api: OmborModule.forRoot(ombor) (testlar) va forRootAsync() (config); AppModule.forRoot tanlaydi.

Keyingi dars: Lifecycle hook'lar — onModuleInit dan onApplicationShutdown gacha; ombor pool'i ilova yopilganda yopiladi va main.ts fabrika yo'liga o'tadi.

Manbalar

  • NestJS hujjati — Dynamic modules (forRoot, forRootAsync, ConfigurableModuleBuilder): https://docs.nestjs.com/fundamentals/dynamic-modules
  • Kanon tajribasi (IlmHamroh, 2026-10-27): ConfigurableModuleBuilder bilan forRoot/forRootAsync, NestJS 12.2.0, Vitest muhitida. Loyiha qadami tekshiruvi — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, Node 24.21.0).
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS dynamic modullar: forRoot, forRootAsync va ConfigurableModuleBuilder — IlmHamroh