Centurion z poprzedniej lekcji przyjmuje rozkazy i przekazuje je dalej. Ale komu? Gdyby sam liczył żołd, sprawdzał zapasy i pisał do skarbca, przestałby być centurionem, a stałby się jednoosobową kohortą - i tę samą pracę trzeba by powtórzyć w każdym innym oddziale.
Legion ma na to specjalistów: kowala, medyka, kwatermistrza. Każdy zna jedną rzecz i robi ją dla wszystkich. W NestJS takim specjalistą jest serwis.
Serwis to zwykła klasa z jednym dekoratorem:
1@Injectable()
2export class LegionService {
3 private legions: Legion[] = [];
4
5 findAll(): Legion[] {
6 return this.legions;
7 }
8
9 findById(id: number): Legion {
10 return this.legions.find((legion) => legion.id === id);
11 }
12
13 recruit(data: CreateLegionDto): Legion {
14 const legion = { id: Date.now(), ...data };
15 this.legions.push(legion);
16 return legion;
17 }
18
19 dismiss(id: number): void {
20 this.legions = this.legions.filter((legion) => legion.id !== id);
21 }
22}
oznacza klasę jako provider, który może być wstrzykiwany. To jedyne, co robi - i warto to odgraniczyć od sąsiadów, bo dekoratory w NestJS wyglądają podobnie: kontroler oznacza @Injectable()
@Controller(), moduł @Module(), a middleware to klasa implementująca NestMiddleware. @Injectable() nie czyni z klasy żadnego z nich - mówi tylko, że NestJS ma umieć ją komuś podać.Cztery metody powyżej to typowy zestaw:
zwraca wszystko, findAll()
jedną pozycję, findById(id)
tworzy nową, recruit(data)
usuwa. Nazwy opisują czynność w dziedzinie, nie czasownik HTTP - bo serwis nie wie, że istnieje jakiś HTTP.dismiss(id)
Gotowego specjalistę kontroler dostaje przez konstruktor:
1@Controller('legions')
2export class LegionController {
3 constructor(private readonly legionService: LegionService) {}
4
5 @Get()
6 findAll() {
7 return this.legionService.findAll();
8 }
9}Kolejność zapisu jest stała:
, potem constructor(
, dalej private readonly legionService:
, na końcu LegionService
.) {}
Ten skrót zasługuje na wyjaśnienie, bo wygląda dziwnie. Słowo
private przed parametrem konstruktora to skrót TypeScriptu: deklaruje pole klasy i przypisuje mu wartość w jednym miejscu. Bez niego musiałbyś napisać private legionService: LegionService osobno i this.legionService = legionService w ciele konstruktora. readonly dokłada gwarancję, że nikt tego pola później nie podmieni.Sam nie tworzysz instancji przez
new - robi to NestJS. Patrzy na typ parametru, znajduje pasujący provider i podaje go gotowego. To jest wstrzykiwanie zależności: klasa mówi, czego potrzebuje, a nie skąd to wziąć.Specjaliści korzystają nawzajem ze swoich usług tak samo. Droga ma cztery kroki i warto ją znać w kolejności:
@Injectable() do nowego serwisu.1@Injectable()
2export class PayrollService {
3 constructor(private readonly legionService: LegionService) {}
4
5 calculateTotalPay(): number {
6 const legions = this.legionService.findAll();
7
8 return legions.reduce((sum, legion) => sum + legion.pay, 0);
9 }
10}Krok drugi jest tym, o którym najczęściej się zapomina. Provider zadeklarowany w module nie jest automatycznie widoczny gdzie indziej - musi być wypisany w
exports swojego modułu, a moduł, który go potrzebuje, musi go zaimportować. Pominięcie tego daje przy starcie błąd o nierozwiązanej zależności.Nasz
LegionService trzyma dane w tablicy. Gdy przyjdzie prawdziwa baza, każda metoda będzie musiała się zmienić - a razem z nią wszystko, co jej używa.Rozwiązaniem jest Repository Pattern - wzorzec, który oddziela logikę dostępu do danych od logiki biznesowej. Serwis mówi wtedy, co chce zrobić, a repozytorium wie, jak dane pobrać. Zmiana tablicy na bazę dotyka wyłącznie repozytorium.
Nie myl go z innymi wzorcami o podobnie brzmiących nazwach: Singleton pilnuje, by istniała jedna instancja, Observer rozsyła powiadomienia o zdarzeniach, Proxy podstawia zastępczy obiekt w miejsce prawdziwego. Tylko Repository dotyczy rozdzielenia danych od logiki.
Zwykle wystarczy podać klasę i NestJS sam ją utworzy. Czasem jednak instancja zależy od czegoś, co znasz dopiero przy starcie - wtedy podajesz przepis zamiast klasy:
1@Module({
2 providers: [
3 {
4 provide: 'STORAGE_SERVICE',
5 useFactory: (config: ConfigService) => {
6 return config.get('STORAGE') === 'cloud'
7 ? new CloudStorageService()
8 : new LocalStorageService();
9 },
10 inject: [ConfigService],
11 },
12 ],
13})
14export class StorageModule {}Trzy pola opisują ten przepis.
to nazwa, pod którą provider będzie dostępny. provide
to funkcja tworząca instancję - tu wybiera między dwiema implementacjami zależnie od konfiguracji. useFactory
wylicza, czego potrzebuje sama fabryka; NestJS poda jej to jako argumenty, w tej samej kolejności.inject
Zauważ, po co to całe zamieszanie: reszta aplikacji prosi o
'STORAGE_SERVICE' i nie wie, którą implementację dostała. Podmiana chmury na dysk lokalny to zmiana w jednym miejscu, @name - a nie w każdym serwisie, który zapisuje pliki.Specjaliści na miejscu, każdy zna swoją rzecz:
@Injectable() oznacza klasę jako provider, który może być wstrzykiwany - nie czyni jej kontrolerem, modułem ani middleware,findAll, findById, recruit, dismiss), nie czasowniki HTTP,constructor(, private readonly nazwa: , TypSerwisu, ) {},private przed parametrem konstruktora deklaruje pole i przypisuje wartość w jednym miejscu,new - NestJS dobiera provider po typie,@Injectable() → import modułu z exports → wstrzyknięcie przez konstruktor → wywołanie metody,exports,provide (nazwa), useFactory (funkcja tworząca) i inject (zależności fabryki),W następnej lekcji zbudujesz z tych elementów pierwsze REST API - komplet operacji CRUD na jednym zasobie. A na razie zapamiętaj: kontroler wie kto ma coś zrobić, serwis wie jak - i tylko dzięki temu tę samą wiedzę wywołasz z dowolnego miejsca aplikacji.