Kurs NestJS · Moduł 1: Podstawy NestJS

Zarządzanie konfiguracją - rejestr prowincji

6 min czytania
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:

  1. Utworzenie pliku .env z kluczami i wartościami.
  2. Rejestracja ConfigModule.forRoot() w module głównym.
  3. Wstrzyknięcie ConfigService przez konstruktor.
  4. 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-key

Format 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/config

Rejestrujemy 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ęcie ConfigService → configService.get('KLUCZ'),
  • zmienne trzymamy w pliku .env - nie config.json, nie settings.yaml, nie environment.xml,
  • format: nazwa, znak równości, wartość; plik nie trafia do repozytorium, commitujesz .env.example,
  • pakietem jest @nestjs/config; @nestjs/env, @nestjs/settings i @nestjs/environment nie istnieją,
  • rejestracja: ConfigModule.forRoot({, opcje oddzielone przecinkami (np. envFilePath: '.env', i isGlobal: true), na końcu }),
  • isGlobal: true udostępnia ConfigService we wszystkich modułach bez dodatkowych importów - nie szyfruje i niczego nie ogranicza,
  • odczyt: this.configService + .get + ('DB_HOST'); wartości z .env przychodzą zawsze jako tekst,
  • drugi argument get to wartość domyślna - nie dawaj jej hasłom ani sekretom,
  • validationSchema z Joi sprawdza 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 w envFilePath wygrywa pierwszy,
  • registerAs('database', () => ({ ... })) grupuje klucze; wczytujesz grupę przez load, odczytujesz przez get('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");
103

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaki pakiet NestJS służy do zarządzania konfiguracją i zmiennymi środowiskowymi?

  2. 2. W jakim pliku przechowujemy zmienne środowiskowe w projekcie NestJS?

  3. 3. Co oznacza ustawienie isGlobal: true w ConfigModule.forRoot()?

  4. 4. Jaki pakiet jest najczęściej używany do walidacji zmiennych środowiskowych w NestJS?

  5. 5. Który plik w folderze legion/ definiuje moduł i spina kontroler z serwisem?

  6. 6. Co jest główną funkcją pliku main.ts w NestJS?

  7. 7. Jaka jest konwencja nazewnictwa plików w NestJS?

  8. 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

Przydatne artykuły