Kurs NestJS · Moduł 1: Podstawy NestJS
Architektura NestJS - budowa Imperium
W tej lekcji7
Pierwszy projekt zwykle mieści się w jednym pliku. Drugi ma ich pięć. Przy dwudziestym nikt już nie wie, gdzie leży obliczanie żołdu - w kontrolerze, w jakimś pomocniku, czy może w trzech miejscach naraz, każde nieco inaczej.
Rzym nie rozrósł się przez dosypywanie ludzi do jednej kohorty. Rozrósł się, bo każdy miał jedną rolę i wszyscy wiedzieli, która to: posłaniec nosi rozkazy, kwatermistrz liczy zapasy, pisarz prowadzi rejestr. NestJS narzuca ten sam podział - i właśnie ta narzucona struktura sprawia, że dwudziesty plik nadal da się znaleźć.
Na czym to stoi
Podstawą architektury NestJS są moduły, kontrolery i providery połączone wstrzykiwaniem zależności (Dependency Injection). Ten układ NestJS zapożyczył z Angulara - dokumentacja frameworka mówi wprost, że jego architektura jest mocno inspirowana Angularem.
Być może słyszałeś o wzorcu Model-View-Controller (MVC). NestJS potrafi w nim pracować, gdy serwer renderuje strony HTML z szablonów, ale to opcjonalna technika, a nie fundament. W typowym backendzie warstwy View w ogóle nie ma - odpowiedzią jest JSON, nie strona HTML.
Wewnątrz NestJS spotkasz też inne znane wzorce, np. każdy provider jest domyślnie singletonem - NestJS tworzy jedną jego instancję na całą aplikację. To jednak szczegół: kształt całej aplikacji wyznaczają moduły i wstrzykiwanie zależności.
Trzy role
Cała aplikacja NestJS składa się z trzech powtarzalnych elementów. Warto je zapamiętać przez pytanie, na które każdy odpowiada:
- Kontroler - kto przyjmuje żądanie? Obsługuje żądania HTTP i zwraca odpowiedzi. Nie przechowuje danych, nie zajmuje się wyglądem, nie kompiluje niczego.
- Serwis - jak to zrobić? Tu mieszka logika.
- Moduł - co należy do siebie? Grupuje jedno i drugie i zgłasza NestJS.
Serwis - gdzie mieszka logika
Zacznijmy od środka, bo to serwis niesie właściwą wartość:
1@Injectable()
2export class LegionService {
3 private legions = [
4 { id: 1, name: 'Legio X Equestris' },
5 { id: 2, name: 'Legio III Gallica' },
6 ];
7
8 findAll() {
9 return this.legions;
10 }
11}Kolejność deklaracji jest stała: @Injectable(), potem export class, dalej nazwa klasy, na końcu ciało z metodami.
I tu pada odpowiedź na pytanie, które w praktyce rozstrzyga o jakości całego projektu: logika biznesowa należy do serwisów - tego, co NestJS nazywa providerami. Nie do kontrolerów, nie do middleware, nie do plików konfiguracyjnych.
Powód jest prosty. Logika w kontrolerze jest przywiązana do HTTP - nie wywołasz jej z zadania wsadowego ani z testu jednostkowego bez udawania żądania. W middleware jest jeszcze gorzej, bo middleware pracuje na surowym żądaniu, zanim wiadomo, dokąd ono trafi. Logika w serwisie jest zwykłą metodą zwykłej klasy - wywołasz ją skąd chcesz.
Kontroler - kto przyjmuje żądanie
Kontroler bierze serwis przez konstruktor i deleguje mu pracę:
1@Controller('legions')
2export class LegionController {
3 constructor(private readonly legionService: LegionService) {}
4
5 @Get()
6 findAll() {
7 return this.legionService.findAll();
8 }
9}Zwróć uwagę na proporcje. Metoda kontrolera ma jedną linię - i tak ma być. Cała jej rola to przyjąć żądanie i wskazać wykonawcę.
Kontroler nie tworzy serwisu przez new. Wpisuje go w konstruktor, a NestJS podaje gotowy - to wstrzykiwanie zależności, mechanizm, który poznasz dokładniej w kolejnych lekcjach. Na razie wystarczy widzieć skutek: kontroler mówi, czego potrzebuje, i nie martwi się, skąd to wziąć.
Moduł - co należy do siebie
Trzeci element spina dwa poprzednie:
1@Module({
2 controllers: [LegionController],
3 providers: [LegionService],
4})
5export class LegionModule {}Bez tego zgłoszenia NestJS nie wie, że te klasy istnieją - pliki same z siebie nic nie znaczą.
Zauważ, że w module widać całą prowincję naraz: kto przyjmuje żądania i kto wykonuje pracę. Otwierając nieznany projekt, moduły czyta się pierwsze, bo to one pokazują mapę.
Droga żądania
Złóżmy to w jeden przebieg. Żądanie GET /legions wędruje tak: trafia do kontrolera LegionController, ten wywołuje metodę serwisu LegionService, serwis zwraca dane, kontroler oddaje je jako odpowiedź. Moduł nie bierze udziału w locie - zadziałał wcześniej, przy starcie, gdy NestJS budował z niego mapę zależności.
Ten schemat powtarza się w całej aplikacji, niezależnie od jej wielkości. Dwudziesty zasób wygląda tak samo jak pierwszy - i dlatego wiadomo, gdzie szukać obliczania żołdu: w serwisie, zawsze w serwisie.
Podsumowanie
Imperium ma administrację, każdy zna swoją rolę:
- podstawą architektury NestJS są moduły, kontrolery i providery połączone wstrzykiwaniem zależności - układ zapożyczony z Angulara,
- MVC to opcjonalna technika renderowania stron z szablonów; w backendzie, który zwraca JSON, warstwy View nie ma,
- kontroler obsługuje żądania HTTP i zwraca odpowiedzi - nie przechowuje danych ani nie zajmuje się wyglądem,
- logika biznesowa należy do serwisów (providerów) - nie do kontrolerów, middleware ani plików konfiguracyjnych,
- powód: logika w serwisie jest zwykłą metodą, więc wywołasz ją także poza HTTP,
- deklaracja serwisu w kolejności:
@Injectable(),export class, nazwa, ciało z metodami, - kontroler przyjmuje serwis przez konstruktor, nie tworzy go przez
new, - metoda kontrolera ma zwykle jedną linię - przyjmij i deleguj,
- moduł zgłasza kontrolery i providery; bez tego NestJS ich nie widzi,
- droga żądania: kontroler → serwis → dane → odpowiedź; moduł działa wcześniej, przy starcie.
W następnej lekcji przyjrzymy się modułom dokładniej - zobaczysz, jak dzielą aplikację na prowincje i co decyduje o tym, że jedna widzi drugą. A na razie zapamiętaj: kontroler przyjmuje, serwis wykonuje, moduł rejestruje - i ten sam układ powtarza się w każdym zakątku aplikacji.
Kod do tej lekcji: src/imperium.module.ts
1// Moduł Imperium - organizacja legionów i podatków
2import { Module } from '@nestjs/common';
3import { LegionController } from './legion.controller';
4import { TributumController } from './tributum.controller';
5import { ProvinciaController } from './provincia.controller';
6import { LegionService } from './legion.service';
7import { TributumService } from './tributum.service';
8import { ProvinciaService } from './provincia.service';
9import { AquaeductService } from './aquaeduct.service';
10
11console.log("Loading Imperium Romanum Architecture...");
12
13// ===========================================
14// 1. Moduł Legionów - Legion Module
15// ===========================================
16@Module({
17 controllers: [LegionController],
18 providers: [LegionService],
19 exports: [LegionService],
20})
21export class LegionModule {
22 constructor() {
23 console.log("Legion Module initialized - Ready to command armies!");
24 }
25}
26
27// ===========================================
28// 2. Moduł Podatków - Tributum Module
29// ===========================================
30@Module({
31 imports: [LegionModule],
32 controllers: [TributumController],
33 providers: [TributumService],
34 exports: [TributumService],
35})
36export class TributumModule {
37 constructor() {
38 console.log("Tributum Module initialized - Ready to collect taxes!");
39 }
40}
41
42// ===========================================
43// 3. Moduł Prowincji - Provincia Module
44// ===========================================
45@Module({
46 imports: [LegionModule, TributumModule],
47 controllers: [ProvinciaController],
48 providers: [ProvinciaService, AquaeductService],
49 exports: [ProvinciaService, AquaeductService],
50})
51export class ProvinciaModule {
52 constructor() {
53 console.log("Provincia Module initialized - All regions ready!");
54 }
55}
56
57// ===========================================
58// 4. Główny moduł aplikacji - App Module
59// ===========================================
60@Module({
61 imports: [
62 LegionModule,
63 TributumModule,
64 ProvinciaModule,
65 ],
66 controllers: [],
67 providers: [
68 {
69 provide: 'IMPERIUM_CONFIG',
70 useValue: {
71 imperiumName: 'Roma Aeterna NestJS',
72 caesar: 'Augustus.js',
73 version: '1.0.0',
74 port: 3000,
75 }
76 }
77 ],
78})
79export class ImperiumAppModule {
80 constructor() {
81 console.log("Imperium App Module loaded successfully!");
82 console.log("All systems operational - Ready for conquest!");
83 }
84}
85
86// ===========================================
87// 5. Demonstracja Dependency Injection
88// ===========================================
89export class DependencyInjectionDemo {
90 static demonstrateModuleArchitecture() {
91 console.log("\n=== ARCHITEKTURA MODUŁÓW ===");
92 console.log("LegionModule: Zarządza legionami");
93 console.log(" ├── LegionController (REST endpoints)");
94 console.log(" ├── LegionService (business logic)");
95 console.log(" └── exports: [LegionService]");
96
97 console.log("\nTributumModule: Zarządza podatkami");
98 console.log(" ├── imports: [LegionModule]");
99 console.log(" ├── TributumController (REST endpoints)");
100 console.log(" ├── TributumService (business logic)");
101 console.log(" └── exports: [TributumService]");
102
103 console.log("\nProvinciaModule: Zarządza prowincjami");
104 console.log(" ├── imports: [LegionModule, TributumModule]");
105 console.log(" ├── ProvinciaController (REST endpoints)");
106 console.log(" ├── ProvinciaService (business logic)");
107 console.log(" ├── AquaeductService (infrastructure logic)");
108 console.log(" └── exports: [ProvinciaService, AquaeductService]");
109
110 console.log("\nImperiumAppModule: Główny moduł");
111 console.log(" ├── imports: [LegionModule, TributumModule, ProvinciaModule]");
112 console.log(" ├── providers: [IMPERIUM_CONFIG]");
113 console.log(" └── Globalne konfiguracje");
114 }
115
116 static demonstrateDependencyFlow() {
117 console.log("\n=== PRZEPŁYW ZALEŻNOŚCI ===");
118 console.log("1. Request → ProvinciaController");
119 console.log("2. ProvinciaController → ProvinciaService (DI)");
120 console.log("3. ProvinciaService → LegionService (DI via import)");
121 console.log("4. ProvinciaService → TributumService (DI via import)");
122 console.log("5. ProvinciaService → AquaeductService (DI)");
123 console.log("6. Response ← ProvinciaController");
124
125 console.log("\nZalety Dependency Injection:");
126 console.log(" Loose coupling (luźne powiązania)");
127 console.log(" Easy testing (łatwe testowanie)");
128 console.log(" Code reusability (wielokrotnego użytku)");
129 console.log(" Maintainability (łatwość utrzymania)");
130 }
131}
132
133DependencyInjectionDemo.demonstrateModuleArchitecture();
134DependencyInjectionDemo.demonstrateDependencyFlow();
135
136export { LegionModule, TributumModule, ProvinciaModule };Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Z jakich elementów składa się architektura aplikacji NestJS?
2. Jaka jest główna rola kontrolera (Controller) w architekturze NestJS?
3. Gdzie według architektury NestJS powinna znajdować się logika biznesowa?
Zadania praktyczne w grze
- Edytor kodu
Napisz serwis LegionService z dekoratorem @Injectable() i metodą findAll(), która zwraca tablicę legionistów (co najmniej jednego), np. [{ id: 1, name: 'Marcus Aurelius' }].
- Układanie w pionie
Ułóż elementy deklaracji serwisu NestJS w prawidłowej kolejności:
- Edytor kodu
Napisz kontroler LegionController z dekoratorem @Controller('legions'), który przez konstruktor przyjmuje LegionService, a w metodzie findAll() z dekoratorem @Get() zwraca wynik this.legionService.findAll().