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ł.
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:
- kto przyjmuje żądania. controllers
- jacy specjaliści tu pracują; to tu rejestrujesz serwisy. providers
- z których innych modułów korzystamy. imports
- co udostępniamy na zewnątrz.exports
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.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.
Skoro moduł rejestruje providerów, możemy złożyć całą drogę zależności. Ma cztery kroki, zawsze w tej kolejności:
@Injectable() - klasa gotowa do wstrzyknięcia.providers modułu - NestJS dowiaduje się, że taka klasa istnieje.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.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
sprawia, że moduł jest dostępny bez importowania. Uwaga na nazwę - nie ma dekoratorów @Global()
@Shared(), @Public() ani @Universal().Jedna rzecz nie zmienia się mimo globalności:
nadal obowiązuje. exports
@Global() zwalnia z importowania modułu, ale to exports decyduje, co ten moduł w ogóle udostępnia.Używaj go oszczędnie, @name. 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.
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 {}
podaje gotową wartość zamiast klasy do utworzenia, a useValue
nadaje jej nazwę - token, po którym będzie rozpoznawana.provide
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}
mówi: podaj mi to, co zarejestrowano pod tą nazwą. Przy zwykłych serwisach dekorator jest zbędny, bo wystarcza typ - tutaj jest konieczny.@Inject('CONFIG_TOKEN')
Prowincje mają swoje administracje, cesarz zna tylko ich listę:
import, { Module }, from, '@nestjs/common',@Module: controllers, providers, imports, exports - pola routes nie ma, trasy wynikają z dekoratorów kontrolera,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,@Injectable() → rejestracja w providers → wstrzyknięcie przez konstruktor → użycie w metodach,@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ą,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.