Kurs NestJS · Moduł 1: Podstawy NestJS

Moduły i Dependency Injection

5 min czytania
W tej lekcji6

Masz już kontroler i serwis. Ale skąd NestJS wie, że ten kontroler istnieje? I skąd bierze serwis, który wstrzykuje mu do konstruktora? Same klasy w plikach nic nie znaczą - ktoś musi je zgłosić.

Imperium nie zarządzało prowincjami z jednego biurka. Każda miała własną administrację: swój spis urzędów, swoich specjalistów i wykaz tego, co udostępnia sąsiadom. Cesarz znał tylko listę prowincji, nie każdego pisarza z osobna. W NestJS taką prowincją jest moduł.

Dekorator @Module

Moduł to klasa z dekoratorem, który wylicza jej zawartość:

1import { Module } from '@nestjs/common';
2
3@Module({
4  imports: [DatabaseModule],
5  controllers: [LegionController],
6  providers: [LegionService],
7  exports: [LegionService],
8})
9export class LegionModule {}

Sam import dekoratora ma stałą postać - import, { Module }, from, '@nestjs/common'.

Cztery pola opisują całą prowincję i każde odpowiada na inne pytanie:

controllers - kto przyjmuje żądania. providers - jacy specjaliści tu pracują; to tu rejestrujesz serwisy. imports - z których innych modułów korzystamy. exports - co udostępniamy na zewnątrz.

To są wszystkie pola dekoratora @Module. Nie ma wśród nich pola routes - trasy nie są konfigurowane w module, tylko wynikają z dekoratorów w kontrolerze, które poznałeś wcześniej.

exports - co widać zza granicy

Pole exports jest tym, które najczęściej sprawia kłopot, więc nazwijmy je wprost: serwis wypisany w exports będzie dostępny dla modułów, które importują ten moduł.

Co z tego wynika w drugą stronę - serwis zarejestrowany w providers, ale nieobecny w exports, działa tylko wewnątrz swojego modułu. Inny moduł, który spróbuje go wstrzyknąć, dostanie przy starcie błąd o nierozwiązanej zależności. To jest właśnie ten błąd, który pojawia się w połowie pierwszych projektów NestJS.

Zwróć uwagę, czego exports nie robi: nie usuwa serwisu z modułu, nie zmienia go w kontroler i nie ma nic wspólnego z zapisem do bazy. To wyłącznie deklaracja widoczności.

Domyślna zamkniętość jest celowa. Moduł ma jawnie powiedzieć, co udostępnia - dzięki temu widać granice, a przypadkowe zależności między odległymi częściami aplikacji nie powstają same z siebie.

Cztery kroki wstrzykiwania

Skoro moduł rejestruje providerów, możemy złożyć całą drogę zależności. Ma cztery kroki, zawsze w tej kolejności:

  1. Zdefiniowanie providera z @Injectable() - klasa gotowa do wstrzyknięcia.
  2. Rejestracja w tablicy providers modułu - NestJS dowiaduje się, że taka klasa istnieje.
  3. Wstrzyknięcie przez konstruktor tam, gdzie jest potrzebna.
  4. Użycie serwisu w metodach klasy.

Krok drugi jest tym, o którym łatwo zapomnieć przy dodawaniu nowego serwisu: sam dekorator @Injectable() nie wystarczy. NestJS buduje mapę zależności z tego, co wypisano w modułach, a nie ze skanowania plików.

Moduł globalny

Niektóre rzeczy przydają się wszędzie - konfiguracja, logger, połączenie z bazą. Importowanie ich modułu w każdym innym module jest wtedy tylko powtarzaniem:

1@Global()
2@Module({
3  providers: [ConfigService],
4  exports: [ConfigService],
5})
6export class ConfigModule {}

Dekorator @Global() sprawia, że moduł jest dostępny bez importowania w pozostałych modułach. Sam moduł globalny rejestrujesz raz, zwykle w imports modułu głównego. Uwaga na nazwę - nie ma dekoratorów @Shared(), @Public() ani @Universal().

Jedna rzecz nie zmienia się mimo globalności: exports nadal obowiązuje. @Global() zwalnia z importowania modułu, ale to exports decyduje, co ten moduł w ogóle udostępnia.

Używaj go oszczędnie. Moduł globalny znika z listy importów, więc przestaje być widać, kto od czego zależy - a to jest informacja, która ratuje przy większym projekcie. Konfiguracja i logger tak; wszystko inne raczej nie.

Provider pod nazwą

Do tej pory providerów rozpoznawaliśmy po typie klasy. Ale wstrzyknąć można też zwykłą wartość - obiekt, tablicę, liczbę - a te typu nie mają:

1@Module({
2  providers: [
3    {
4      provide: 'CONFIG_TOKEN',
5      useValue: { apiUrl: 'https://api.imperium.rome', timeout: 5000 },
6    },
7  ],
8})
9export class AppModule {}

useValue podaje gotową wartość zamiast klasy do utworzenia, a provide nadaje jej nazwę - token, po którym będzie rozpoznawana.

Skoro nie ma tu typu, po którym NestJS mógłby dobrać provider, przy wstrzykiwaniu wskazujemy token wprost:

1@Injectable()
2export class ApiService {
3  constructor(@Inject('CONFIG_TOKEN') private config: { apiUrl: string }) {}
4}

@Inject('CONFIG_TOKEN') mówi: podaj mi to, co zarejestrowano pod tą nazwą. Przy zwykłych serwisach dekorator jest zbędny, bo wystarcza typ - tutaj jest konieczny.

Podsumowanie

Prowincje mają swoje administracje, cesarz zna tylko ich listę:

  • moduł grupuje powiązane elementy i zgłasza je NestJS - same klasy w plikach nic nie znaczą,
  • import dekoratora: import, { Module }, from, '@nestjs/common',
  • cztery pola @Module: controllers, providers, imports, exports - pola routes nie ma, trasy wynikają z dekoratorów kontrolera,
  • serwis w exports jest dostępny dla modułów importujących ten moduł; bez tego działa tylko wewnętrznie,
  • exports niczego nie usuwa ani nie zmienia - to sama deklaracja widoczności,
  • cztery kroki wstrzykiwania: @Injectable() → rejestracja w providers → wstrzyknięcie przez konstruktor → użycie w metodach,
  • sam @Injectable() nie wystarczy - NestJS buduje mapę zależności z modułów, nie ze skanowania plików,
  • @Global() udostępnia moduł bez importowania; @Shared(), @Public() i @Universal() nie istnieją,
  • globalność nie zwalnia z exports - i stosuj ją oszczędnie, bo ukrywa zależności,
  • useValue rejestruje gotową wartość, provide nadaje jej token, a @Inject('TOKEN') wskazuje go przy wstrzykiwaniu.

W następnej lekcji poznasz kontrolery od środka - zobaczysz, jak żądanie trafia do właściwej metody. A na razie zapamiętaj: moduł to administracja prowincji - rejestruje, kto tu pracuje, i jawnie mówi, czego użyczy sąsiadom.

Kod do tej lekcji: src/legion.module.ts
1// Moduły i Dependency Injection - Organizacja Legionu
2import { Module } from '@nestjs/common';
3import { LegionController } from './legion.controller';
4import { LegionService } from './legion.service';
5import { ArmorService } from './armor.service';
6
7console.log("Organizacja legionu przez moduły i DI!");
8
9// ===========================================
10// 1. Podstawowy moduł - struktura legionu
11// ===========================================
12
13@Module({
14  controllers: [LegionController],
15  providers: [LegionService, ArmorService],
16  exports: [LegionService], // Udostępniamy dla innych modułów
17})
18export class LegionModule {}
19
20// ===========================================
21// 2. Moduł prowincji importujący LegionModule
22// ===========================================
23
24import { ProvinciaController } from './provincia.controller';
25import { ProvinciaService } from './provincia.service';
26
27@Module({
28  imports: [LegionModule], // Import daje dostęp do exports
29  controllers: [ProvinciaController],
30  providers: [ProvinciaService],
31})
32export class ProvinciaModule {}
33
34// ===========================================
35// 3. Główny moduł Imperium
36// ===========================================
37
38@Module({
39  imports: [LegionModule, ProvinciaModule],
40})
41export class AppModule {}
42
43// ===========================================
44// 4. Dependency Injection w praktyce
45// ===========================================
46
47import { Injectable } from '@nestjs/common';
48
49@Injectable()
50export class ArmorService {
51  getArmorStrength(type: string): number {
52    const armorMap: Record<string, number> = {
53      lorica_segmentata: 90,
54      lorica_hamata: 70,
55      lorica_squamata: 60,
56      scutum: 50,
57    };
58    return armorMap[type] || 30;
59  }
60}
61
62@Injectable()
63export class LegionService {
64  // DI - NestJS automatycznie wstrzykuje ArmorService
65  constructor(private readonly armorService: ArmorService) {
66    console.log("LegionService utworzony z ArmorService!");
67  }
68
69  calculateDefense(armorType: string, soldiers: number): number {
70    const armorStrength = this.armorService.getArmorStrength(armorType);
71    return armorStrength * soldiers;
72  }
73}
74
75console.log("\n=== DEPENDENCY INJECTION ===");
76console.log("1. @Module deklaruje controllers, providers, exports");
77console.log("2. imports pozwala korzystać z exports innego modułu");
78console.log("3. NestJS automatycznie wstrzykuje zależności przez konstruktor");
79console.log("4. @Injectable() oznacza klasę jako provider");
80

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Które z poniższych pól NIE jest częścią dekoratora @Module() w NestJS?

  2. 2. Co oznacza umieszczenie serwisu w tablicy 'exports' modułu?

  3. 3. Jaki dekorator sprawia, że moduł jest dostępny globalnie bez potrzeby importowania?

Zadania praktyczne w grze

  • Edytor kodu

    Zdefiniuj moduł LegionModule z dekoratorem @Module(), który rejestruje LegionController w controllers, LegionService w providers i udostępnia LegionService innym modułom przez exports.

  • Układanie w pionie

    Uporządkuj kroki procesu Dependency Injection w NestJS od początku do końca:

  • Klikanie w kolejności

    Ułóż elementy importu dekoratora Module w prawidłowej kolejności:

  • Edytor kodu

    Zdefiniuj stałą configProvider z provide: 'CONFIG_TOKEN' i useValue z obiektem konfiguracji (np. { legion: 'Legio X Equestris' }) i dodaj ją do providers modułu AppModule. Napisz serwis LegionConfigService, który wstrzykuje tę wartość przez konstruktor dekoratorem @Inject('CONFIG_TOKEN') i zwraca ją w metodzie getConfig().

Przydatne artykuły