NestJS

Provider va Dependency Injection

Agar NestJS'ning bitta eng muhim g'oyasini tanlash kerak bo'lsa, bu Dependency Injection (bog'liqliklarni injektsiya qilish, qisqacha DI) bo'lardi. Bu tushuncha boshida sehrli tuyulishi mumkin, lekin aslida u juda oddiy va kuchli. Ushbu darsda DI qanday ishlashini, provayderlar nima ekanligini va service'larni modullar orasida qanday ulashishni to'liq o'rganamiz.

Muammo: bog'liqliklarni qo'lda boshqarish

Avval DI nima uchun kerakligini tushunamiz. Faraz qilaylik, UsersService ma'lumotlar bazasi bilan ishlash uchun DatabaseService'ga bog'liq. DI'siz biz uni qo'lda yaratamiz:

// DI SIZ (yomon usul):
class UsersService {
  private db: DatabaseService;

  constructor() {
    // Bog'liqlikni o'zimiz yaratamiz โ€” bu qattiq bog'lanish:
    this.db = new DatabaseService('localhost', 5432, 'parol');
  }
}

Bu yondashuvning muammolari:

Yechim: Dependency Injection

DI g'oyasi juda oddiy: obyekt o'ziga kerakli bog'liqliklarni o'zi yaratmaydi, balki ularni tashqaridan qabul qiladi. Kim yaratadi? NestJS'ning maxsus tizimi โ€” IoC konteyner (Inversion of Control โ€” nazoratni teskari qilish).

DI bilan yuqoridagi kod shunday bo'ladi. E'tibor bering, UsersService endi DatabaseService'ni qanday yaratishni bilmaydi โ€” u shunchaki so'raydi:

@Injectable()
class UsersService {
  // Bog'liqlikni tashqaridan qabul qilamiz, o'zimiz yaratmaymiz:
  constructor(private readonly db: DatabaseService) {}

  // NestJS db'ni avtomatik topib, shu yerga uzatadi
}
Bu "nazoratni teskari qilish" nomining ma'nosi: odatda kod o'z bog'liqliklarini o'zi nazorat qiladi (yaratadi). DI'da esa bu nazorat teskari yo'nalishga o'tadi โ€” framework bog'liqliklarni yaratib, kodga beradi.

Konstruktor injektsiyasi qanday ishlaydi?

NestJS'da eng keng tarqalgan usul โ€” konstruktor injektsiyasi. Siz service'ni konstruktor argumenti sifatida e'lon qilasiz, NestJS uning tipini ko'radi va tegishli nusxani avtomatik uzatadi. Jarayon quyidagicha:

  1. Siz konstruktorda private readonly db: DatabaseService yozasiz;
  2. NestJS TypeScript metama'lumotlari orqali dbning tipi DatabaseService ekanligini biladi;
  3. IoC konteyner o'z ichida DatabaseService nusxasi bor-yo'qligini tekshiradi;
  4. Agar bor bo'lsa, o'shani uzatadi; yo'q bo'lsa โ€” yangi yaratib, keyingi safar uchun eslab qoladi.
@Injectable()
export class UsersService {
  constructor(
    private readonly db: DatabaseService,
    private readonly logger: LoggerService,
  ) {}
  // Ikkala bog'liqlik ham avtomatik injektsiya qilinadi

  async findAll() {
    this.logger.log('Foydalanuvchilar so'raldi');
    return this.db.query('SELECT * FROM users');
  }
}
private readonly yozuvi TypeScript'ning qulay xususiyati: u argumentni avtomatik ravishda klass xossasiga aylantiradi. Ya'ni this.db = db deb qo'lda yozish shart emas. Bu NestJS'da konstruktor injektsiyasi juda ixcham ko'rinishining sababi.

Provider nima?

Provider โ€” bu NestJS injektsiya qila oladigan har qanday narsa. Ko'pincha bu @Injectable service, lekin u qiymat, funksiya yoki tashqi kutubxona ham bo'lishi mumkin. Provider modulning providers massivida ro'yxatdan o'tishi kerak:

@Module({
  providers: [UsersService, DatabaseService, LoggerService],
})
export class UsersModule {}

Standart holatda har bir provider singleton โ€” ya'ni butun ilova bo'ylab uning faqat bitta nusxasi mavjud bo'ladi. DatabaseService'ni 100 ta joyda ishlatsangiz ham, xotirada bitta nusxa bo'ladi va u barcha service'lar orasida ulashiladi. Bu resurslarni tejaydi.

Singleton โ€” bu NestJS DI'ning muhim afzalligi. Masalan, bazaga ulanish (connection pool) bir marta yaratilib, butun ilova bo'ylab qayta ishlatiladi. Bu Express'da qo'lda global o'zgaruvchilar bilan qilinadigan ishni avtomatlashtiradi.

Maxsus provayderlar (custom providers)

Ba'zan siz oddiy klassni emas, balki maxsus qiymat yoki fabrikani injektsiya qilmoqchi bo'lasiz. NestJS buning uchun maxsus provayder sintaksisini beradi. Masalan, konfiguratsiya qiymatini token orqali berish:

@Module({
  providers: [
    // Oddiy service:
    UsersService,

    // Qiymat provayderi (masalan, konfiguratsiya):
    {
      provide: 'API_KALIT',
      useValue: 'maxfiy-kalit-12345',
    },

    // Fabrika provayderi (dinamik qiymat):
    {
      provide: 'DB_ULANISH',
      useFactory: () => {
        return new DatabaseService('localhost', 5432);
      },
    },
  ],
})
export class UsersModule {}

Token orqali berilgan provayderni olish uchun @Inject dekoratori ishlatiladi (chunki string token'ni TypeScript tipi orqali aniqlab bo'lmaydi):

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(
    @Inject('API_KALIT') private readonly apiKalit: string,
  ) {}
}

Modullararo import va export

Endi eng muhim mavzuga o'tamiz: bir modulning service'ini boshqa modulda qanday ishlatish mumkin? Standart holatda modulning provayderlari faqat o'sha modul ichida ko'rinadi. Ularni tashqariga chiqarish uchun ikki bosqich bor.

1-bosqich: service'ni beruvchi modulda uni exports massiviga qo'shamiz. Masalan, DatabaseModule o'z DatabaseService'ini eksport qiladi:

// src/database/database.module.ts
import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],   // Boshqa modullar uchun ochamiz
})
export class DatabaseModule {}

2-bosqich: service'ni ishlatmoqchi bo'lgan modulda DatabaseModule'ni imports'ga qo'shamiz:

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { DatabaseModule } from '../database/database.module';

@Module({
  imports: [DatabaseModule],   // DatabaseService'ni olish uchun
  providers: [UsersService],
})
export class UsersModule {}

Endi UsersService ichida DatabaseService'ni bemalol injektsiya qilishimiz mumkin. NestJS uni DatabaseModule'dan topadi:

@Injectable()
export class UsersService {
  // DatabaseService boshqa moduldan keladi, lekin bemalol ishlaydi:
  constructor(private readonly db: DatabaseService) {}
}
Agar service'ni exports'ga qo'shmasangiz, uni boshqa modulda ishlatib bo'lmaydi โ€” NestJS "Nest can't resolve dependencies" xatosini beradi. Ikki qoidani yodda tuting: beruvchi modul exports qilishi, oluvchi modul esa uni imports qilishi shart.

Global modullar

Ba'zi modullar (masalan, konfiguratsiya yoki logger) deyarli hamma joyda kerak bo'ladi. Ularni har bir modulga alohida import qilish zerikarli. Bunday holatda modulni @Global() deb belgilash mumkin โ€” shunda uni faqat bir marta import qilib, hamma joyda ishlatish mumkin:

import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';

@Global()   // Bu modul global bo'ladi
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}
@Global()'ni ehtiyotkorlik bilan ishlating. Hamma narsani global qilish modullararo bog'liqliklarni yashirin qiladi va kodni tushunish qiyinlashadi. Uni faqat haqiqatan ham hamma joyda kerak bo'ladigan (config, logger) modullar uchun ishlating.

Xulosa