Kurs NestJS · Moduł 1: Podstawy NestJS
Serwisy i providery - specjaliści Imperium
W tej lekcji6
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.
Dekorator, który czyni specjalistę
Serwis to zwykła klasa z jednym dekoratorem:
1@Injectable()
2export class LegionService {
3 private legions: Legion[] = [];
4 private nextId = 1;
5
6 findAll(): Legion[] {
7 return this.legions;
8 }
9
10 findById(id: number): Legion {
11 return this.legions.find((legion) => legion.id === id);
12 }
13
14 recruit(data: CreateLegionDto): Legion {
15 const legion = { id: this.nextId++, ...data };
16 this.legions.push(legion);
17 return legion;
18 }
19
20 dismiss(id: number): void {
21 this.legions = this.legions.filter((legion) => legion.id !== id);
22 }
23}@Injectable() 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 @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: findAll() zwraca wszystko, findById(id) jedną pozycję, recruit(data) tworzy nową, dismiss(id) usuwa. Nazwy opisują czynność w dziedzinie, nie czasownik HTTP - bo serwis nie wie, że istnieje jakiś HTTP.
Wstrzyknięcie do kontrolera
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: constructor(, potem private readonly legionService: , dalej LegionService, na końcu ) {}.
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ąć.
Serwis w serwisie
Specjaliści korzystają nawzajem ze swoich usług tak samo. Droga ma cztery kroki i warto ją znać w kolejności:
- Dodanie
@Injectable()do nowego serwisu. - Import modułu z eksportowanym serwisem - jeśli pochodzi z innego modułu.
- Wstrzyknięcie serwisu przez konstruktor.
- Wywołanie metody wstrzykniętego 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.
Oddzielenie dostępu do danych
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.
Provider z fabryki
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. provide to nazwa, pod którą provider będzie dostępny. useFactory to funkcja tworząca instancję - tu wybiera między dwiema implementacjami zależnie od konfiguracji. inject wylicza, czego potrzebuje sama fabryka; NestJS poda jej to jako argumenty, w tej samej kolejności.
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 - a nie w każdym serwisie, który zapisuje pliki.
Podsumowanie
Specjaliści na miejscu, każdy zna swoją rzecz:
- serwis trzyma logikę, kontroler tylko kieruje żądania,
@Injectable()oznacza klasę jako provider, który może być wstrzykiwany - nie czyni jej kontrolerem, modułem ani middleware,- nazwy metod opisują czynność w dziedzinie (
findAll,findById,recruit,dismiss), nie czasowniki HTTP, - wstrzyknięcie w kolejności:
constructor(,private readonly nazwa:,TypSerwisu,) {}, privateprzed parametrem konstruktora deklaruje pole i przypisuje wartość w jednym miejscu,- instancji nie tworzysz przez
new- NestJS dobiera provider po typie, - serwis w serwisie:
@Injectable()→ import modułu zexports→ wstrzyknięcie przez konstruktor → wywołanie metody, - provider nie jest widoczny poza swoim modułem, dopóki nie trafi do
exports, - Repository Pattern oddziela logikę dostępu do danych od logiki biznesowej - Singleton, Observer i Proxy rozwiązują zupełnie inne problemy,
- provider z fabryki opisują
provide(nazwa),useFactory(funkcja tworząca) iinject(zależności fabryki), - dzięki fabryce reszta aplikacji nie wie, którą implementację dostała.
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.
Kod do tej lekcji: src/legion.service.ts
1// Serwisy i Providers - Specjaliści w Imperium
2import { Injectable, NotFoundException } from '@nestjs/common';
3
4console.log("Serwisy NestJS - specjaliści wykonujący prawdziwą pracę!");
5
6// ===========================================
7// 1. Podstawowy serwis z @Injectable
8// ===========================================
9
10interface Legionary {
11 id: number;
12 name: string;
13 rank: string;
14 experience: number;
15 legion: string;
16}
17
18@Injectable()
19export class LegionService {
20 private legionaries: Legionary[] = [
21 { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', experience: 10, legion: 'Legio X' },
22 { id: 2, name: 'Julia Domna', rank: 'Optio', experience: 8, legion: 'Legio X' },
23 { id: 3, name: 'Titus Flavius', rank: 'Miles', experience: 3, legion: 'Legio III' },
24 ];
25
26 findAll(): Legionary[] {
27 return this.legionaries;
28 }
29
30 findById(id: number): Legionary {
31 const legionary = this.legionaries.find(l => l.id === id);
32 if (!legionary) {
33 throw new NotFoundException('Legionary non inventus!');
34 }
35 return legionary;
36 }
37
38 create(data: Omit<Legionary, 'id'>): Legionary {
39 const newLegionary: Legionary = {
40 id: Math.max(...this.legionaries.map(l => l.id)) + 1,
41 ...data,
42 };
43 this.legionaries.push(newLegionary);
44 return newLegionary;
45 }
46
47 update(id: number, data: Partial<Legionary>): Legionary {
48 const legionary = this.findById(id);
49 Object.assign(legionary, data);
50 return legionary;
51 }
52
53 remove(id: number): void {
54 const index = this.legionaries.findIndex(l => l.id === id);
55 if (index === -1) throw new NotFoundException('Legionary non inventus!');
56 this.legionaries.splice(index, 1);
57 }
58
59 // Logika biznesowa - obliczanie siły legionu
60 calculateLegionStrength(legionName: string): number {
61 const soldiers = this.legionaries.filter(l => l.legion === legionName);
62 return soldiers.reduce((sum, s) => sum + s.experience * 10, 0);
63 }
64}
65
66// ===========================================
67// 2. Serwis z wstrzykniętym innym serwisem
68// ===========================================
69
70@Injectable()
71export class RankService {
72 private rankHierarchy: Record<string, number> = {
73 Miles: 1,
74 Optio: 2,
75 Centurio: 3,
76 Tribunus: 4,
77 Legatus: 5,
78 };
79
80 getRankLevel(rank: string): number {
81 return this.rankHierarchy[rank] || 0;
82 }
83
84 canPromote(currentRank: string, targetRank: string): boolean {
85 return this.getRankLevel(targetRank) === this.getRankLevel(currentRank) + 1;
86 }
87}
88
89@Injectable()
90export class PromotionService {
91 constructor(
92 private readonly legionService: LegionService,
93 private readonly rankService: RankService,
94 ) {}
95
96 promote(legionaryId: number, newRank: string): Legionary {
97 const legionary = this.legionService.findById(legionaryId);
98 if (!this.rankService.canPromote(legionary.rank, newRank)) {
99 throw new Error('Awans niemożliwy - niewłaściwa ranga!');
100 }
101 return this.legionService.update(legionaryId, { rank: newRank });
102 }
103}
104
105console.log("\n=== PODSUMOWANIE SERWISÓW ===");
106console.log("@Injectable() - oznacza klasę jako provider");
107console.log("Serwisy zawierają logikę biznesową");
108console.log("DI pozwala wstrzykiwać serwisy do siebie nawzajem");
109console.log("Kontrolery delegują pracę do serwisów");
110Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służy dekorator @Injectable() w NestJS?
2. Jaki wzorzec projektowy oddziela logikę dostępu do danych od logiki biznesowej?
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj kroki tworzenia serwisu współpracującego z innym serwisem:
- Edytor kodu
Napisz serwis LegionService z prywatną tablicą legionistów i metodami: findAll() zwraca tablicę, findById(id) zwraca legionistę o tym id (albo undefined), recruit(data) dodaje legionistę z nowym, niepowtarzalnym id i go zwraca, dismiss(id) usuwa legionistę o tym id.
- Klikanie w kolejności
Ułóż elementy wstrzykiwania serwisu przez konstruktor kontrolera:
- Edytor kodu
Zdefiniuj stałą storageProvider z provide: 'STORAGE_SERVICE', inject: [ConfigService] i useFactory, która dostaje ConfigService i zwraca new CloudStorageService(), gdy config.get('STORAGE') === 'cloud', a w przeciwnym razie new LocalStorageService(). Obie klasy napisz sam, a storageProvider dodaj do providers modułu StorageModule.