Kurs NestJS · Moduł 1: Podstawy NestJS

Architektura NestJS - budowa Imperium

5 min czytania
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. 1. Z jakich elementów składa się architektura aplikacji NestJS?

  2. 2. Jaka jest główna rola kontrolera (Controller) w architekturze NestJS?

  3. 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().

Przydatne artykuły