Kurs NestJS · Moduł 1: Podstawy NestJS
Moduły i Dependency Injection
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:
- Zdefiniowanie providera z
@Injectable()- klasa gotowa do wstrzyknięcia. - Rejestracja w tablicy
providersmodułu - NestJS dowiaduje się, że taka klasa istnieje. - Wstrzyknięcie przez konstruktor tam, gdzie jest potrzebna.
- 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- polaroutesnie ma, trasy wynikają z dekoratorów kontrolera, - serwis w
exportsjest dostępny dla modułów importujących ten moduł; bez tego działa tylko wewnętrznie, exportsniczego nie usuwa ani nie zmienia - to sama deklaracja widoczności,- cztery kroki wstrzykiwania:
@Injectable()→ rejestracja wproviders→ 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, useValuerejestruje gotową wartość,providenadaje 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");
80Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Które z poniższych pól NIE jest częścią dekoratora @Module() w NestJS?
2. Co oznacza umieszczenie serwisu w tablicy 'exports' modułu?
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().