Kurs NestJS · Moduł 1: Podstawy NestJS
Zarządzanie konfiguracją - rejestr prowincji
W tej lekcji8
Adres bazy danych wpisany w kod działa doskonale - na Twoim laptopie. Na serwerze testowym baza stoi gdzie indziej, na produkcyjnym jeszcze gdzie indziej, a hasło do niej nie powinno w ogóle trafić do repozytorium. Trzy środowiska, jeden kod, trzy różne wartości - i żadnej nie da się wpisać na stałe.
Rzym miał na to osobny zwyczaj. Rozkazy pisano raz, ale zasoby prowincji spisywano w miejscowym rejestrze: ile zboża, którędy droga, kto zarządza. Ten sam legion wchodził do Galii i do Egiptu, czytając za każdym razem tamtejszy rejestr. W aplikacji tym rejestrem są zmienne środowiskowe.
Cztery kroki
Konfiguracja wchodzi do projektu w ustalonej kolejności - warto ją znać jako całość, bo pominięcie kroku daje mylące błędy:
- Utworzenie pliku
.envz kluczami i wartościami. - Rejestracja
ConfigModule.forRoot()w module głównym. - Wstrzyknięcie
ConfigServiceprzez konstruktor. - Odczyt wartości przez
configService.get('KLUCZ').
Krok pierwszy: plik .env
Zmienne środowiskowe trzymamy w pliku .env w katalogu projektu:
1DB_HOST=localhost
2DB_PORT=5432
3DB_PASSWORD=tribute123
4JWT_SECRET=super-secret-keyFormat jest minimalny: nazwa, znak równości, wartość. Bez cudzysłowów, bez spacji wokół równa się, bez średników na końcu.
Nazwa pliku nie jest umowna - to .env, a nie config.json, settings.yaml czy environment.xml. Te formaty spotkasz w innych ekosystemach, ale nie tutaj.
Sam plik nigdy nie trafia do repozytorium - dopisujesz go do .gitignore. Zamiast niego commitujesz .env.example z tymi samymi kluczami i pustymi lub przykładowymi wartościami, żeby kolega wiedział, co ma uzupełnić.
Krok drugi: rejestracja modułu
Obsługę konfiguracji daje pakiet @nestjs/config. Uwaga na nazwę - nie ma pakietów @nestjs/env, @nestjs/settings ani @nestjs/environment. Instalujesz go poleceniem:
1npm i @nestjs/configRejestrujemy go w module głównym:
1@Module({
2 imports: [
3 ConfigModule.forRoot({
4 envFilePath: '.env',
5 isGlobal: true,
6 }),
7 ],
8})
9export class AppModule {}Zapis ma stały szkielet: ConfigModule.forRoot({, potem opcje - tu envFilePath: '.env', i isGlobal: true - a na końcu }). Kolejność opcji w obiekcie nie ma znaczenia; ważne, żeby oddzielał je przecinek.
envFilePath wskazuje plik do wczytania; przy domyślnej nazwie można go pominąć.
isGlobal: true sprawia, że ConfigService jest dostępny we wszystkich modułach bez dodatkowych importów. Bez tej opcji musiałbyś importować ConfigModule w każdym module z osobna - a używa go zwykle połowa aplikacji. Zwróć uwagę, czego ta opcja nie robi: niczego nie szyfruje, nie ogranicza widoczności do modułu głównego i nie odświeża konfiguracji przy restarcie.
Kroki trzeci i czwarty: odczyt
ConfigService wstrzykujesz jak każdą inną zależność:
1@Injectable()
2export class DatabaseService {
3 constructor(private configService: ConfigService) {}
4
5 getConnection() {
6 const host = this.configService.get('DB_HOST');
7 const port = this.configService.get<number>('DB_PORT', 5432);
8
9 return { host, port };
10 }
11}Odczyt składa się z trzech członów w stałej kolejności: this.configService, .get, ('DB_HOST').
Dwa szczegóły warto znać. Zapis <number> to parametr typu - mówi TypeScriptowi, czego się spodziewasz; wartości z pliku .env przychodzą bowiem zawsze jako tekst. Drugi argument, tutaj 5432, to wartość domyślna użyta, gdy klucza brak. To wygodne, ale ostrożnie: domyślne hasło albo domyślny klucz JWT to gotowa luka - dla takich wartości lepiej, żeby aplikacja nie wstała, niż żeby wstała z byle czym.
Sprawdzenie przy starcie
Skoro brak klucza daje undefined, awaria ujawni się dopiero przy pierwszym użyciu - czasem po godzinach. Lepiej sprawdzić komplet od razu:
1ConfigModule.forRoot({
2 isGlobal: true,
3 validationSchema: Joi.object({
4 DB_HOST: Joi.string().required(),
5 DB_PORT: Joi.number().default(5432),
6 JWT_SECRET: Joi.string().required(),
7 }),
8});validationSchema opisuje, jakich kluczy oczekujesz i jakiego typu. Przy starcie ConfigModule porówna z nim zawartość .env i przerwie uruchomienie, jeśli czegoś brakuje - z komunikatem mówiącym wprost, którego klucza.
To zamiana awarii o trzeciej w nocy na błąd przy wdrożeniu. Joi to osobna biblioteka opisu schematów, nie część NestJS: instalujesz ją poleceniem npm i joi (NestJS 12 wymaga Joi w wersji 18 lub nowszej) i importujesz przez import * as Joi from 'joi';. @nestjs/config przyjmuje taki schemat w opcji validationSchema.
Zauważ, że każdy klucz ma albo .required(), albo .default(...). Połączenie obu nie ma sensu: gdy wymaganego klucza brak, walidacja zatrzyma start, zanim wartość domyślna zdążyłaby zadziałać.
Skąd pochodzi wartość
Ten sam klucz może stać w kilku miejscach naraz. Gdy jest i w zmiennych środowiskowych systemu (np. ustawionych na serwerze), i w pliku .env, wygrywa zmienna systemowa - plik jej nie nadpisze. NestJS sam wczytuje tylko .env; inne pliki podajesz w envFilePath, np. envFilePath: ['.env.production', '.env'], i wtedy przy powtórzonym kluczu wygrywa pierwszy plik z listy. Wartość domyślna z configService.get('KLUCZ', domyślna) działa dopiero wtedy, gdy klucza nie ma nigdzie.
Grupy konfiguracji: registerAs
Gdy kluczy przybywa, wygodnie jest zgrupować je tematycznie. Funkcja registerAs z @nestjs/config tworzy nazwaną grupę:
1import { registerAs } from '@nestjs/config';
2
3export const databaseConfig = registerAs('database', () => ({
4 host: process.env.DB_HOST,
5 port: parseInt(process.env.DB_PORT ?? '5432', 10),
6 name: process.env.DB_NAME,
7}));Pierwszy argument to nazwa grupy, drugi - fabryka, która zwraca obiekt z wartościami. Zwróć uwagę na parseInt: zmienne środowiskowe są tekstem, więc liczby zamieniasz sam. Grupę wczytujesz opcją load - ConfigModule.forRoot({ load: [databaseConfig] }) - a odczytujesz po kropce: this.configService.get('database.host').
Podsumowanie
Rejestr prowincji czytany na miejscu, kod jeden dla wszystkich:
- konfiguracja mieszka poza kodem, bo ta sama aplikacja działa w kilku środowiskach,
- cztery kroki po kolei: plik
.env→ConfigModule.forRoot()w module głównym → wstrzyknięcieConfigService→configService.get('KLUCZ'), - zmienne trzymamy w pliku
.env- nieconfig.json, niesettings.yaml, nieenvironment.xml, - format: nazwa, znak równości, wartość; plik nie trafia do repozytorium, commitujesz
.env.example, - pakietem jest
@nestjs/config;@nestjs/env,@nestjs/settingsi@nestjs/environmentnie istnieją, - rejestracja:
ConfigModule.forRoot({, opcje oddzielone przecinkami (np.envFilePath: '.env',iisGlobal: true), na końcu}), isGlobal: trueudostępniaConfigServicewe wszystkich modułach bez dodatkowych importów - nie szyfruje i niczego nie ogranicza,- odczyt:
this.configService+.get+('DB_HOST'); wartości z.envprzychodzą zawsze jako tekst, - drugi argument
getto wartość domyślna - nie dawaj jej hasłom ani sekretom, validationSchemazJoisprawdza komplet kluczy przy starcie i przerywa uruchomienie, gdy czegoś brak; klucz ma.required()albo.default(...), nie oba,- zmienna systemowa wygrywa z plikiem
.env, a przy kilku plikach wenvFilePathwygrywa pierwszy, registerAs('database', () => ({ ... }))grupuje klucze; wczytujesz grupę przezload, odczytujesz przezget('database.host').
To ostatnia lekcja tego modułu. Umiesz już zbudować moduł, kontroler i serwis, wystawić REST API, opisać dane przez DTO i wyprowadzić konfigurację poza kod - czyli wszystko, czego trzeba, by aplikacja NestJS ruszyła w świat. A na razie zapamiętaj: kod jest jeden dla wszystkich prowincji; różni je tylko rejestr, który czytają na miejscu.
Kod do tej lekcji: src/config-management.ts
1// Configuration Management - Zarządzanie Zasobami Imperium
2import { Module, Injectable } from '@nestjs/common';
3
4console.log("Configuration Management - kwatermistrz imperium!");
5
6// ===========================================
7// 1. ConfigModule - podstawowa konfiguracja
8// ===========================================
9
10// app.module.ts
11// import { ConfigModule } from '@nestjs/config';
12//
13// @Module({
14// imports: [
15// ConfigModule.forRoot({
16// isGlobal: true, // Dostępne w całym imperium
17// envFilePath: '.env', // Ścieżka do pliku konfiguracji
18// }),
19// ],
20// })
21// export class AppModule {}
22
23// ===========================================
24// 2. Użycie ConfigService
25// ===========================================
26
27// import { ConfigService } from '@nestjs/config';
28
29@Injectable()
30export class LegionConfigService {
31 // W prawdziwej aplikacji: constructor(private configService: ConfigService)
32
33 getPort(): number {
34 // return this.configService.get<number>('PORT', 3000);
35 return 4000; // Domyślny port imperium
36 }
37
38 getDatabaseUrl(): string {
39 // return this.configService.get<string>('DATABASE_URL');
40 return 'mongodb://localhost:27017/imperium';
41 }
42
43 getJwtSecret(): string {
44 // return this.configService.get<string>('JWT_SECRET');
45 return 'roma-aeterna-secret';
46 }
47
48 isProduction(): boolean {
49 // return this.configService.get('NODE_ENV') === 'production';
50 return false;
51 }
52}
53
54// ===========================================
55// 3. Custom configuration files
56// ===========================================
57
58// config/legion.config.ts
59export const legionConfig = () => ({
60 legion: {
61 maxSoldiers: 6000,
62 minExperience: 1,
63 ranks: ['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'],
64 defaultProvince: 'Roma',
65 },
66 database: {
67 host: process.env.DB_HOST || 'localhost',
68 port: parseInt(process.env.DB_PORT || '5432', 10),
69 name: process.env.DB_NAME || 'imperium_db',
70 },
71 security: {
72 jwtExpiration: '7d',
73 bcryptRounds: 12,
74 maxLoginAttempts: 5,
75 },
76});
77
78// ===========================================
79// 4. Walidacja konfiguracji z Joi
80// ===========================================
81
82// import * as Joi from 'joi';
83//
84// ConfigModule.forRoot({
85// validationSchema: Joi.object({
86// NODE_ENV: Joi.string()
87// .valid('development', 'production', 'test')
88// .default('development'),
89// PORT: Joi.number().default(4000),
90// DATABASE_URL: Joi.string().required(),
91// JWT_SECRET: Joi.string().required(),
92// }),
93// validationOptions: {
94// abortEarly: true,
95// },
96// })
97
98console.log("\n=== PODSUMOWANIE CONFIGURATION ===");
99console.log("ConfigModule.forRoot() - ładowanie konfiguracji");
100console.log("ConfigService.get() - pobieranie wartości");
101console.log("load: [config] - własne pliki konfiguracji");
102console.log("validationSchema - walidacja zmiennych środowiskowych");
103Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki pakiet NestJS służy do zarządzania konfiguracją i zmiennymi środowiskowymi?
2. W jakim pliku przechowujemy zmienne środowiskowe w projekcie NestJS?
3. Co oznacza ustawienie isGlobal: true w ConfigModule.forRoot()?
4. Jaki pakiet jest najczęściej używany do walidacji zmiennych środowiskowych w NestJS?
5. Który plik w folderze legion/ definiuje moduł i spina kontroler z serwisem?
6. Co jest główną funkcją pliku main.ts w NestJS?
7. Jaka jest konwencja nazewnictwa plików w NestJS?
8. Jaka jest prawidłowa kolejność przetwarzania żądania HTTP w NestJS?
Zadania praktyczne w grze
- Edytor kodu
Zarejestruj ConfigModule.forRoot({ isGlobal: true }) w imports modułu AppModule i napisz serwis DatabaseService, który przez konstruktor dostaje ConfigService, a w metodzie getHost() zwraca this.configService.get('DB_HOST').
- Układanie w pionie
Uporządkuj kroki konfiguracji zmiennych środowiskowych od pierwszego do ostatniego:
- Klikanie w kolejności
Ułóż elementy rejestracji ConfigModule w prawidłowej kolejności:
- Układanie w poziomie
Ułóż elementy odczytania zmiennej środowiskowej przez ConfigService:
- Edytor kodu
Napisz i wyeksportuj stałą databaseConfig = registerAs('database', ...), której fabryka zwraca obiekt { host, port, name } z wartościami process.env.DB_HOST, process.env.DB_PORT (zamienionym na liczbę) i process.env.DB_NAME.
- Układanie w pionie
Aplikacja rejestruje ConfigModule.forRoot({ envFilePath: ['.env.production', '.env'] }). Uporządkuj źródła wartości tego samego klucza od najwyższego priorytetu do najniższego:
- Układanie w poziomie
Ułóż elementy walidacji schematu Joi dla zmiennej PORT:
- Klikanie w kolejności
Ułóż elementy definicji zmiennej środowiskowej w pliku .env:
- Układanie w pionie
Uporządkuj etapy uruchamiania aplikacji NestJS od początku do końca:
- Edytor kodu
W imports modułu AppModule dodaj ConfigModule.forRoot() z opcją validationSchema: schemat Joi.object(), w którym PORT jest liczbą (Joi.number() z wartością domyślną 3000), a DATABASE_URL wymaganym tekstem (Joi.string().required()).
- Układanie w poziomie
Ułóż elementy importu ConfigModule w prawidłowej kolejności:
- Układanie w poziomie
Ułóż elementy tworzenia instancji aplikacji NestJS:
- Klikanie w kolejności
Ułóż elementy cyklu życia żądania HTTP w NestJS w prawidłowej kolejności:
- Edytor kodu
Stwórz funkcję bootstrap, która tworzy aplikację NestFactory.create(), ustawia globalny ValidationPipe i nasłuchuje na porcie 3000