17 Configuration Management

Stellen Sie sich vor, Sie ziehen in eine neue Wohnung. Sie müssen wissen, wo die Lichtschalter sind, wie die Heizung funktioniert, welche Schlüssel zu welchen Türen gehören und wo sich die wichtigsten Dinge befinden. Genauso braucht jede Anwendung Konfigurationsinformationen - wo befindet sich die Datenbank, welche API-Schlüssel soll sie verwenden, wie soll sie sich in verschiedenen Umgebungen verhalten?

Configuration Management ist wie ein gut organisiertes Adressbuch für Ihre Anwendung. Es sorgt dafür, dass Ihre App immer weiß, wie sie sich verhalten soll, egal ob sie auf Ihrem Entwicklungsrechner, einem Testserver oder in der Produktionsumgebung läuft. In diesem Kapitel werden wir gemeinsam lernen, wie wir Konfigurationen so verwalten, dass sie sicher, wartbar und umgebungsspezifisch sind.

17.1 Warum Configuration Management so wichtig ist

Denken Sie an ein Chamäleon, das seine Farbe je nach Umgebung ändert. Eine gut konfigurierte Anwendung sollte sich ähnlich verhalten - in der Entwicklung sollte sie ausführliche Logs ausgeben und vielleicht eine lokale Datenbank verwenden, während sie in der Produktion optimiert, sicher und mit echten Datenbanken arbeitet.

Früher haben Entwickler oft Konfigurationswerte direkt in den Code geschrieben - das war, als würde man die Adresse seines Hauses in Stein meißeln. Wenn sich etwas änderte, musste der gesamte Code neu kompiliert und deployed werden. Heute wissen wir es besser: Konfiguration und Code müssen getrennt sein.

17.1.1 Die zwölf Faktoren der App-Konfiguration

Die “Twelve-Factor App” Methodologie lehrt uns wichtige Prinzipien für moderne Anwendungen. Der dritte Faktor besagt: “Speichere die Konfiguration in der Umgebung”. Das bedeutet, dass sich verhaltensändernde Einstellungen nie im Code befinden sollten, sondern immer von außen kommen müssen.

// Schlecht: Konfiguration im Code
@Injectable()
export class DatabaseService {
  private readonly connectionString = 'postgresql://user:password@localhost:5432/myapp';
  // Was passiert, wenn wir auf einen anderen Server umziehen?
  // Was passiert, wenn sich das Passwort ändert?
  // Wie testen wir mit verschiedenen Datenbanken?
}

// Gut: Konfiguration von außen
@Injectable()
export class DatabaseService {
  constructor(
    @Inject('DATABASE_CONFIG') private config: DatabaseConfig
  ) {}
  
  // Die Verbindungsdetails kommen von außen und können sich ändern,
  // ohne dass wir den Code anfassen müssen
}

17.2 Environment Variables - Die Grundbausteine der Konfiguration

Environment Variables sind wie Notizzettel, die Sie an verschiedenen Orten hinterlassen. Ihr Betriebssystem verwaltet sie, und jede Anwendung kann sie lesen. Sie sind perfekt für Konfigurationswerte, weil sie einfach zu setzen sind und von der Anwendung getrennt bleiben.

17.2.1 Die Anatomie von Environment Variables verstehen

Environment Variables sind einfache Schlüssel-Wert-Paare. Stellen Sie sich vor, Sie haben einen Spind mit verschiedenen Fächern, und jedes Fach hat einen Namen und enthält einen Zettel mit einer Information. So funktionieren Environment Variables - sie haben einen Namen (wie DATABASE_URL) und einen Wert (wie postgresql://localhost:5432/myapp).

// Verschiedene Arten von Environment Variables
export interface EnvironmentVariables {
  // Basis-Anwendungseinstellungen
  NODE_ENV: 'development' | 'production' | 'test';
  PORT: number;
  
  // Datenbank-Konfiguration
  DATABASE_URL: string;
  DATABASE_HOST: string;
  DATABASE_PORT: number;
  DATABASE_USERNAME: string;
  DATABASE_PASSWORD: string;
  DATABASE_NAME: string;
  
  // API-Schlüssel und Secrets
  JWT_SECRET: string;
  JWT_EXPIRES_IN: string;
  STRIPE_SECRET_KEY: string;
  SENDGRID_API_KEY: string;
  
  // Feature-Flags
  ENABLE_REGISTRATION: boolean;
  ENABLE_EMAIL_VERIFICATION: boolean;
  ENABLE_TWO_FACTOR_AUTH: boolean;
  
  // Performance und Verhalten
  CACHE_TTL: number;
  MAX_UPLOAD_SIZE: number;
  RATE_LIMIT_MAX: number;
  RATE_LIMIT_WINDOW: number;
  
  // Externe Services
  REDIS_URL: string;
  ELASTICSEARCH_URL: string;
  S3_BUCKET_NAME: string;
  S3_REGION: string;
}

17.2.2 .env Dateien verstehen und richtig verwenden

.env Dateien sind wie persönliche Notizblöcke für Entwickler. Sie erlauben es uns, Environment Variables in einer Datei zu definieren, anstatt sie einzeln zu setzen. Das ist besonders nützlich während der Entwicklung, wo wir viele verschiedene Einstellungen ausprobieren möchten.

# .env.example - Diese Datei gehört ins Version Control
# Sie zeigt anderen Entwicklern, welche Variablen benötigt werden

# Basis-Konfiguration
NODE_ENV=development
PORT=3000

# Datenbank (lokale Entwicklung)
DATABASE_URL=postgresql://localhost:5432/myapp_dev
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=developer
DATABASE_PASSWORD=dev_password
DATABASE_NAME=myapp_dev

# JWT-Konfiguration
JWT_SECRET=your_jwt_secret_here_minimum_32_characters
JWT_EXPIRES_IN=1h

# Feature-Flags
ENABLE_REGISTRATION=true
ENABLE_EMAIL_VERIFICATION=false
ENABLE_TWO_FACTOR_AUTH=false

# Performance-Einstellungen
CACHE_TTL=300
MAX_UPLOAD_SIZE=5242880
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=900000

# Externe Services (Entwicklung)
REDIS_URL=redis://localhost:6379
SENDGRID_API_KEY=your_sendgrid_key_here
STRIPE_SECRET_KEY=sk_test_your_stripe_test_key_here
# .env.development - Spezifische Entwicklungseinstellungen
NODE_ENV=development
PORT=3000

# Lokale Entwicklungsdatenbank
DATABASE_URL=postgresql://dev_user:dev_pass@localhost:5432/myapp_dev

# Entwicklungs-spezifische Einstellungen
LOG_LEVEL=debug
ENABLE_CORS=true
ENABLE_SWAGGER=true

# Mock-Services für Entwicklung
USE_MOCK_EMAIL_SERVICE=true
USE_MOCK_PAYMENT_SERVICE=true
# .env.production - Produktionseinstellungen (NIEMALS ins Version Control!)
NODE_ENV=production
PORT=80

# Produktionsdatenbank (echte Credentials)
DATABASE_URL=postgresql://prod_user:super_secure_password@db.company.com:5432/myapp_prod

# Produktions-Secrets
JWT_SECRET=super_long_and_complex_production_secret_key_with_random_characters_123456789
STRIPE_SECRET_KEY=sk_live_real_stripe_production_key

# Produktions-optimierte Einstellungen
LOG_LEVEL=error
ENABLE_CORS=false
ENABLE_SWAGGER=false
CACHE_TTL=3600

17.2.3 Umgebungsspezifische Konfiguration verstehen

Verschiedene Umgebungen haben verschiedene Bedürfnisse, wie verschiedene Anlässe verschiedene Kleidung erfordern. In der Entwicklung wollen wir ausführliche Informationen und einfache Debugging-Möglichkeiten. Im Testing brauchen wir isolierte, reproduzierbare Bedingungen. In der Produktion steht Performance und Sicherheit im Vordergrund.

// Umgebungsspezifische Konfigurationsfactory
export const configurationFactory = () => {
  const env = process.env.NODE_ENV || 'development';
  
  // Basis-Konfiguration, die für alle Umgebungen gilt
  const baseConfig = {
    app: {
      name: process.env.APP_NAME || 'MyNestJSApp',
      version: process.env.APP_VERSION || '1.0.0',
      port: parseInt(process.env.PORT, 10) || 3000,
    },
    database: {
      url: process.env.DATABASE_URL,
      host: process.env.DATABASE_HOST || 'localhost',
      port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
      username: process.env.DATABASE_USERNAME,
      password: process.env.DATABASE_PASSWORD,
      name: process.env.DATABASE_NAME,
    },
  };

  // Umgebungsspezifische Überschreibungen
  const environmentConfigs = {
    development: {
      app: {
        ...baseConfig.app,
        debug: true,
        logLevel: 'debug',
      },
      database: {
        ...baseConfig.database,
        synchronize: true, // Automatische Schema-Synchronisation in der Entwicklung
        logging: true,     // SQL-Queries loggen
        dropSchema: false, // Nicht automatisch das Schema löschen
      },
      cors: {
        enabled: true,
        origins: ['http://localhost:3000', 'http://localhost:4200'],
        credentials: true,
      },
      swagger: {
        enabled: true,
        path: 'api-docs',
      },
      email: {
        provider: 'mock', // Mock-E-Mail-Service für Entwicklung
        from: 'noreply@localhost',
      },
    },
    
    test: {
      app: {
        ...baseConfig.app,
        debug: false,
        logLevel: 'error', // Nur Fehler loggen während Tests
      },
      database: {
        type: 'sqlite',
        database: ':memory:', // In-Memory-Datenbank für Tests
        synchronize: true,
        logging: false,      // Keine DB-Logs während Tests
        dropSchema: true,    // Schema für jeden Test zurücksetzen
      },
      cors: {
        enabled: false, // Keine CORS-Probleme in Tests
      },
      swagger: {
        enabled: false, // Swagger nicht in Tests
      },
      email: {
        provider: 'mock',
        from: 'test@example.com',
      },
    },
    
    production: {
      app: {
        ...baseConfig.app,
        debug: false,
        logLevel: 'warn', // Nur Warnungen und Fehler
      },
      database: {
        ...baseConfig.database,
        synchronize: false, // NIEMALS automatische Schema-Änderungen in Produktion!
        logging: false,     // Keine SQL-Logs aus Performance-Gründen
        ssl: {
          rejectUnauthorized: false, // Oft nötig für Cloud-Datenbanken
        },
        pool: {
          min: 5,  // Mindestens 5 Verbindungen im Pool
          max: 20, // Maximal 20 Verbindungen
        },
      },
      cors: {
        enabled: true,
        origins: [process.env.FRONTEND_URL], // Nur echte Frontend-URL
        credentials: true,
      },
      swagger: {
        enabled: false, // Swagger nicht in Produktion aus Sicherheitsgründen
      },
      email: {
        provider: 'sendgrid',
        apiKey: process.env.SENDGRID_API_KEY,
        from: process.env.EMAIL_FROM,
      },
      security: {
        rateLimiting: {
          enabled: true,
          max: 100,
          windowMs: 15 * 60 * 1000, // 15 Minuten
        },
        helmet: {
          enabled: true,
        },
      },
    },
  };

  // Merge base config with environment-specific config
  return {
    ...baseConfig,
    ...environmentConfigs[env],
    environment: env,
  };
};

17.3 NestJS Configuration Service - Der zentrale Konfigurationsmanager

Der NestJS Configuration Service ist wie ein sehr gut organisierter Bibliothekar, der genau weiß, wo jede Information steht und sie auf Anfrage schnell bereitstellen kann. Er macht es einfach, Konfigurationswerte in der ganzen Anwendung zu verwenden, ohne sich Gedanken über die Quelle zu machen.

17.3.1 Grundlegende Configuration Service Implementierung

// configuration.module.ts - Der zentrale Ort für alle Konfiguration
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { configurationFactory } from './configuration.factory';
import { validationSchema } from './configuration.validation';

@Module({
  imports: [
    ConfigModule.forRoot({
      // Lade verschiedene .env Dateien basierend auf der Umgebung
      envFilePath: [
        `.env.${process.env.NODE_ENV}`, // z.B. .env.development
        '.env.local',                   // Lokale Überschreibungen
        '.env',                         // Fallback
      ],
      
      // Konfigurationsfactory verwenden
      load: [configurationFactory],
      
      // Validierung der Konfiguration
      validationSchema,
      validationOptions: {
        allowUnknown: true, // Erlaube unbekannte Umgebungsvariablen
        abortEarly: false,  // Sammle alle Validierungsfehler
      },
      
      // Mache ConfigService global verfügbar
      isGlobal: true,
      
      // Cache-Einstellungen
      cache: true,
      
      // Erweitere process.env um unsere Konfiguration
      expandVariables: true,
    }),
  ],
  providers: [
    // Custom Configuration Services können hier hinzugefügt werden
  ],
  exports: [ConfigModule],
})
export class ConfigurationModule {}

17.3.2 Typisierte Konfiguration für bessere Entwicklererfahrung

Typisierte Konfiguration ist wie ein Wörterbuch mit klaren Definitionen - es verhindert Missverständnisse und macht Fehler zur Compile-Zeit sichtbar statt zur Laufzeit.

// configuration.types.ts - Typen für unsere Konfiguration
export interface DatabaseConfig {
  url: string;
  host: string;
  port: number;
  username: string;
  password: string;
  name: string;
  synchronize: boolean;
  logging: boolean;
  ssl?: {
    rejectUnauthorized: boolean;
  };
  pool?: {
    min: number;
    max: number;
  };
}

export interface AppConfig {
  name: string;
  version: string;
  port: number;
  debug: boolean;
  logLevel: 'debug' | 'info' | 'warn' | 'error';
}

export interface SecurityConfig {
  jwtSecret: string;
  jwtExpiresIn: string;
  rateLimiting: {
    enabled: boolean;
    max: number;
    windowMs: number;
  };
  helmet: {
    enabled: boolean;
  };
}

export interface EmailConfig {
  provider: 'mock' | 'sendgrid' | 'smtp';
  apiKey?: string;
  from: string;
  smtp?: {
    host: string;
    port: number;
    username: string;
    password: string;
  };
}

export interface Configuration {
  app: AppConfig;
  database: DatabaseConfig;
  security: SecurityConfig;
  email: EmailConfig;
  environment: string;
}

// configuration.service.ts - Typisierter Configuration Service
@Injectable()
export class TypedConfigService {
  constructor(private configService: ConfigService<Configuration>) {}

  // App-Konfiguration
  get appConfig(): AppConfig {
    return this.configService.get('app', { infer: true });
  }

  get appName(): string {
    return this.configService.get('app.name', { infer: true });
  }

  get appPort(): number {
    return this.configService.get('app.port', { infer: true });
  }

  get isProduction(): boolean {
    return this.configService.get('environment') === 'production';
  }

  get isDevelopment(): boolean {
    return this.configService.get('environment') === 'development';
  }

  get isTest(): boolean {
    return this.configService.get('environment') === 'test';
  }

  // Datenbank-Konfiguration
  get databaseConfig(): DatabaseConfig {
    return this.configService.get('database', { infer: true });
  }

  get databaseUrl(): string {
    return this.configService.get('database.url', { infer: true });
  }

  // Sicherheits-Konfiguration
  get jwtSecret(): string {
    const secret = this.configService.get('security.jwtSecret', { infer: true });
    if (!secret) {
      throw new Error('JWT_SECRET ist nicht konfiguriert');
    }
    return secret;
  }

  get jwtExpiresIn(): string {
    return this.configService.get('security.jwtExpiresIn', { infer: true }) || '1h';
  }

  // E-Mail-Konfiguration
  get emailConfig(): EmailConfig {
    return this.configService.get('email', { infer: true });
  }

  // Feature-Flags
  get isFeatureEnabled(featureName: string): boolean {
    return this.configService.get(`features.${featureName}`, false);
  }

  // Helper-Methoden für häufige Abfragen
  get logLevel(): string {
    return this.configService.get('app.logLevel', 'info');
  }

  get maxUploadSize(): number {
    return this.configService.get('app.maxUploadSize', 5 * 1024 * 1024); // 5MB default
  }

  // Sichere Methode um Secrets zu holen
  getSecret(key: string): string {
    const value = this.configService.get(key);
    if (!value) {
      throw new Error(`Secret '${key}' ist nicht konfiguriert`);
    }
    return value;
  }

  // Sichere Methode um optionale Konfiguration zu holen
  getOptional<T>(key: string, defaultValue: T): T {
    return this.configService.get(key, defaultValue);
  }
}

17.3.3 Verwendung der Konfiguration in Services

// Beispiel: Database Service mit typisierter Konfiguration
@Injectable()
export class DatabaseService {
  constructor(private config: TypedConfigService) {}

  async createConnection(): Promise<DataSource> {
    const dbConfig = this.config.databaseConfig;
    
    return new DataSource({
      type: 'postgres',
      url: dbConfig.url,
      host: dbConfig.host,
      port: dbConfig.port,
      username: dbConfig.username,
      password: dbConfig.password,
      database: dbConfig.name,
      synchronize: dbConfig.synchronize,
      logging: dbConfig.logging,
      ssl: dbConfig.ssl,
      entities: [/* Ihre Entities */],
      migrations: [/* Ihre Migrations */],
    });
  }
}

// Beispiel: Email Service mit umgebungsabhängigem Verhalten
@Injectable()
export class EmailService {
  private emailProvider: EmailProvider;

  constructor(private config: TypedConfigService) {
    this.initializeEmailProvider();
  }

  private initializeEmailProvider(): void {
    const emailConfig = this.config.emailConfig;

    switch (emailConfig.provider) {
      case 'mock':
        this.emailProvider = new MockEmailProvider();
        break;
      case 'sendgrid':
        this.emailProvider = new SendGridProvider(emailConfig.apiKey);
        break;
      case 'smtp':
        this.emailProvider = new SmtpProvider(emailConfig.smtp);
        break;
      default:
        throw new Error(`Unbekannter E-Mail-Provider: ${emailConfig.provider}`);
    }
  }

  async sendEmail(to: string, subject: string, content: string): Promise<void> {
    const from = this.config.emailConfig.from;
    
    if (this.config.isDevelopment) {
      console.log(`[DEV] E-Mail würde gesendet werden an ${to}: ${subject}`);
    }

    await this.emailProvider.send({ from, to, subject, content });
  }
}

17.4 Schema Validation für Konfiguration

Schema Validation für Konfiguration ist wie ein Qualitätsprüfer in einer Fabrik - er stellt sicher, dass alles, was hineingeht, den Standards entspricht. Wenn ein wichtiger Konfigurationswert fehlt oder falsch ist, erfahren wir es sofort beim Start der Anwendung, nicht erst wenn ein Benutzer einen Fehler auslöst.

17.4.1 Joi-basierte Validation

Joi ist eine bewährte Bibliothek für Schema-Validation in JavaScript. Sie ist wie ein sehr pedantischer Lehrer, der jeden Wert genau überprüft und klare Fehlermeldungen gibt, wenn etwas nicht stimmt.

// configuration.validation.ts - Validierung mit Joi
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  // Umgebung
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test')
    .default('development')
    .description('Die Anwendungsumgebung'),

  // App-Grundeinstellungen
  PORT: Joi.number()
    .port() // Muss ein gültiger Port sein (1-65535)
    .default(3000)
    .description('Port auf dem die Anwendung läuft'),

  APP_NAME: Joi.string()
    .min(1)
    .max(100)
    .default('MyNestJSApp')
    .description('Name der Anwendung'),

  // Datenbank-Konfiguration
  DATABASE_URL: Joi.string()
    .uri()
    .required()
    .description('Vollständige Datenbank-URL'),

  DATABASE_HOST: Joi.string()
    .hostname()
    .when('DATABASE_URL', {
      is: Joi.exist(),
      then: Joi.optional(),
      otherwise: Joi.required(),
    })
    .description('Datenbank-Host'),

  DATABASE_PORT: Joi.number()
    .port()
    .default(5432)
    .description('Datenbank-Port'),

  DATABASE_USERNAME: Joi.string()
    .min(1)
    .when('DATABASE_URL', {
      is: Joi.exist(),
      then: Joi.optional(),
      otherwise: Joi.required(),
    })
    .description('Datenbank-Benutzername'),

  DATABASE_PASSWORD: Joi.string()
    .min(1)
    .when('DATABASE_URL', {
      is: Joi.exist(),
      then: Joi.optional(),
      otherwise: Joi.required(),
    })
    .description('Datenbank-Passwort'),

  DATABASE_NAME: Joi.string()
    .min(1)
    .when('DATABASE_URL', {
      is: Joi.exist(),
      then: Joi.optional(),
      otherwise: Joi.required(),
    })
    .description('Datenbank-Name'),

  // JWT-Konfiguration
  JWT_SECRET: Joi.string()
    .min(32) // Mindestens 32 Zeichen für Sicherheit
    .required()
    .description('Geheimer Schlüssel für JWT-Token (min. 32 Zeichen)'),

  JWT_EXPIRES_IN: Joi.string()
    .pattern(/^\d+[smhd]$/) // Muster wie "1h", "30m", "7d"
    .default('1h')
    .description('JWT-Token Ablaufzeit (z.B. "1h", "30m", "7d")'),

  // E-Mail-Konfiguration
  EMAIL_PROVIDER: Joi.string()
    .valid('mock', 'sendgrid', 'smtp')
    .default('mock')
    .description('E-Mail-Provider'),

  SENDGRID_API_KEY: Joi.string()
    .when('EMAIL_PROVIDER', {
      is: 'sendgrid',
      then: Joi.required(),
      otherwise: Joi.optional(),
    })
    .description('SendGrid API-Schlüssel'),

  EMAIL_FROM: Joi.string()
    .email()
    .required()
    .description('Absender-E-Mail-Adresse'),

  // Redis-Konfiguration
  REDIS_URL: Joi.string()
    .uri({ scheme: ['redis', 'rediss'] })
    .optional()
    .description('Redis-Verbindungs-URL'),

  // Feature-Flags
  ENABLE_REGISTRATION: Joi.boolean()
    .default(true)
    .description('Aktiviert/Deaktiviert Benutzerregistrierung'),

  ENABLE_TWO_FACTOR_AUTH: Joi.boolean()
    .default(false)
    .description('Aktiviert/Deaktiviert Zwei-Faktor-Authentifizierung'),

  // Performance-Einstellungen
  CACHE_TTL: Joi.number()
    .min(0)
    .max(86400) // Maximal 24 Stunden
    .default(300) // 5 Minuten
    .description('Cache Time-To-Live in Sekunden'),

  MAX_UPLOAD_SIZE: Joi.number()
    .min(1024) // Mindestens 1KB
    .max(100 * 1024 * 1024) // Maximal 100MB
    .default(5 * 1024 * 1024) // Standard 5MB
    .description('Maximale Upload-Größe in Bytes'),

  RATE_LIMIT_MAX: Joi.number()
    .min(1)
    .max(10000)
    .default(100)
    .description('Maximale Requests pro Zeitfenster'),

  RATE_LIMIT_WINDOW: Joi.number()
    .min(1000) // Mindestens 1 Sekunde
    .max(3600000) // Maximal 1 Stunde
    .default(900000) // 15 Minuten
    .description('Rate Limiting Zeitfenster in Millisekunden'),
});

// Erweiterte Validation mit Custom Rules
export const advancedValidationSchema = validationSchema.keys({
  // Custom Validation: URL muss HTTPS in Produktion sein
  FRONTEND_URL: Joi.string()
    .uri()
    .when('NODE_ENV', {
      is: 'production',
      then: Joi.string().pattern(/^https:\/\//),
      otherwise: Joi.string().uri(),
    })
    .required()
    .description('Frontend-URL (muss HTTPS in Produktion sein)'),

  // Custom Validation: Log Level abhängig von Umgebung
  LOG_LEVEL: Joi.string()
    .valid('debug', 'info', 'warn', 'error')
    .when('NODE_ENV', {
      is: 'production',
      then: Joi.valid('warn', 'error').default('warn'),
      otherwise: Joi.default('debug'),
    })
    .description('Log-Level'),

  // Conditional Validation: SSL-Optionen nur in Produktion erforderlich
  DATABASE_SSL_CERT: Joi.string()
    .when('NODE_ENV', {
      is: 'production',
      then: Joi.required(),
      otherwise: Joi.optional(),
    })
    .description('SSL-Zertifikat für Datenbankverbindung'),

}).custom((value, helpers) => {
  // Custom Validation Logic
  const { NODE_ENV, JWT_SECRET, DATABASE_PASSWORD } = value;

  // Zusätzliche Sicherheitsprüfungen für Produktion
  if (NODE_ENV === 'production') {
    if (JWT_SECRET.length < 64) {
      return helpers.error('any.custom', {
        message: 'JWT_SECRET muss in Produktion mindestens 64 Zeichen haben',
      });
    }

    if (DATABASE_PASSWORD && DATABASE_PASSWORD.length < 12) {
      return helpers.error('any.custom', {
        message: 'DATABASE_PASSWORD muss in Produktion mindestens 12 Zeichen haben',
      });
    }
  }

  return value;
});

17.4.2 Class-Validator basierte Validation

Eine Alternative zu Joi ist class-validator, das besonders gut mit TypeScript funktioniert und Decorators verwendet, die wir bereits aus anderen Teilen von NestJS kennen.

// configuration.dto.ts - Validation mit class-validator
import { 
  IsString, 
  IsNumber, 
  IsEnum, 
  IsOptional, 
  IsUrl, 
  IsEmail, 
  Min, 
  Max, 
  MinLength,
  IsBoolean,
  ValidateIf
} from 'class-validator';
import { Transform, Type } from 'class-transformer';

export enum Environment {
  Development = 'development',
  Production = 'production',
  Test = 'test',
}

export enum EmailProvider {
  Mock = 'mock',
  SendGrid = 'sendgrid',
  SMTP = 'smtp',
}

export class ConfigurationDto {
  @IsEnum(Environment)
  @IsOptional()
  NODE_ENV: Environment = Environment.Development;

  @IsNumber()
  @Min(1)
  @Max(65535)
  @Type(() => Number)
  @IsOptional()
  PORT: number = 3000;

  @IsString()
  @MinLength(1)
  @IsOptional()
  APP_NAME: string = 'MyNestJSApp';

  // Datenbank-Konfiguration
  @IsUrl()
  DATABASE_URL: string;

  @IsString()
  @IsOptional()
  @ValidateIf((o) => !o.DATABASE_URL)
  DATABASE_HOST: string;

  @IsNumber()
  @Min(1)
  @Max(65535)
  @Type(() => Number)
  @IsOptional()
  DATABASE_PORT: number = 5432;

  @IsString()
  @MinLength(1)
  @IsOptional()
  @ValidateIf((o) => !o.DATABASE_URL)
  DATABASE_USERNAME: string;

  @IsString()
  @MinLength(1)
  @IsOptional()
  @ValidateIf((o) => !o.DATABASE_URL)
  DATABASE_PASSWORD: string;

  @IsString()
  @MinLength(1)
  @IsOptional()
  @ValidateIf((o) => !o.DATABASE_URL)
  DATABASE_NAME: string;

  // JWT-Konfiguration
  @IsString()
  @MinLength(32, { message: 'JWT_SECRET muss mindestens 32 Zeichen haben' })
  JWT_SECRET: string;

  @IsString()
  @IsOptional()
  @Transform(({ value }) => value || '1h')
  JWT_EXPIRES_IN: string = '1h';

  // E-Mail-Konfiguration
  @IsEnum(EmailProvider)
  @IsOptional()
  EMAIL_PROVIDER: EmailProvider = EmailProvider.Mock;

  @IsString()
  @IsOptional()
  @ValidateIf((o) => o.EMAIL_PROVIDER === EmailProvider.SendGrid)
  SENDGRID_API_KEY?: string;

  @IsEmail()
  EMAIL_FROM: string;

  // Feature-Flags
  @IsBoolean()
  @Transform(({ value }) => value === 'true' || value === true)
  @IsOptional()
  ENABLE_REGISTRATION: boolean = true;

  @IsBoolean()
  @Transform(({ value }) => value === 'true' || value === true)
  @IsOptional()
  ENABLE_TWO_FACTOR_AUTH: boolean = false;

  // Performance-Einstellungen
  @IsNumber()
  @Min(0)
  @Max(86400)
  @Type(() => Number)
  @IsOptional()
  CACHE_TTL: number = 300;

  @IsNumber()
  @Min(1024)
  @Max(100 * 1024 * 1024)
  @Type(() => Number)
  @IsOptional()
  MAX_UPLOAD_SIZE: number = 5 * 1024 * 1024;

  // Custom Validation Methoden
  @IsProductionSecure()
  validateProductionSecurity(): boolean {
    if (this.NODE_ENV === Environment.Production) {
      return this.JWT_SECRET.length >= 64;
    }
    return true;
  }
}

// Custom Decorator für Production-spezifische Validierung
function IsProductionSecure() {
  return function (object: any, propertyName: string) {
    // Implementation des Custom Validators
  };
}

17.5 Multi-Environment Setup - Verschiedene Welten für Ihre Anwendung

Multi-Environment Setup ist wie das Haben verschiedener Wohnungen für verschiedene Anlässe. Sie haben eine gemütliche Wohnung für den Alltag (Entwicklung), ein ordentliches Gästezimmer für Besucher (Testing) und vielleicht ein elegantes Büro für wichtige Geschäftstermine (Produktion). Jede Umgebung hat ihre eigenen Regeln und Einrichtungen.

17.5.1 Environment-spezifische Konfigurationsdateien

// config/environments/development.ts
export const developmentConfig = {
  app: {
    debug: true,
    logLevel: 'debug',
    enableSwagger: true,
    enableCors: true,
  },
  database: {
    synchronize: true,
    logging: true,
    dropSchema: false,
    migrationsRun: false,
  },
  security: {
    rateLimiting: {
      enabled: false, // Kein Rate Limiting in der Entwicklung
    },
    helmet: {
      enabled: false, // Vereinfachte Entwicklung
    },
  },
  email: {
    provider: 'mock',
    logEmails: true, // E-Mails in Console loggen
  },
  cache: {
    ttl: 60, // Kurze Cache-Zeit für schnelle Entwicklung
  },
  features: {
    enableRegistration: true,
    enablePasswordReset: true,
    enableEmailVerification: false, // Vereinfacht Tests
    enableTwoFactorAuth: false,
  },
};

// config/environments/test.ts
export const testConfig = {
  app: {
    debug: false,
    logLevel: 'error', // Nur Fehler während Tests
    enableSwagger: false,
    enableCors: false,
  },
  database: {
    type: 'sqlite',
    database: ':memory:',
    synchronize: true,
    logging: false,
    dropSchema: true, // Für jeden Test saubere Datenbank
  },
  security: {
    rateLimiting: {
      enabled: false, // Keine Rate Limits in Tests
    },
  },
  email: {
    provider: 'mock',
    logEmails: false,
  },
  cache: {
    ttl: 1, // Sehr kurze Cache-Zeit für deterministische Tests
  },
  features: {
    enableRegistration: true,
    enablePasswordReset: true,
    enableEmailVerification: true, // Alle Features testen
    enableTwoFactorAuth: true,
  },
};

// config/environments/staging.ts
export const stagingConfig = {
  app: {
    debug: false,
    logLevel: 'info',
    enableSwagger: true, // Für API-Tests verfügbar
    enableCors: true,
  },
  database: {
    synchronize: false, // Migrations verwenden
    logging: false,
    ssl: {
      rejectUnauthorized: false,
    },
  },
  security: {
    rateLimiting: {
      enabled: true,
      max: 1000, // Großzügiger als Produktion
      windowMs: 15 * 60 * 1000,
    },
    helmet: {
      enabled: true,
    },
  },
  email: {
    provider: 'sendgrid',
    useTestCredentials: true, // Test-E-Mail-Credentials
  },
  cache: {
    ttl: 300, // 5 Minuten
  },
  features: {
    enableRegistration: true,
    enablePasswordReset: true,
    enableEmailVerification: true,
    enableTwoFactorAuth: false, // Noch nicht in Staging
  },
};

// config/environments/production.ts
export const productionConfig = {
  app: {
    debug: false,
    logLevel: 'warn', // Nur Warnungen und Fehler
    enableSwagger: false, // Sicherheitsrisiko in Produktion
    enableCors: false, // Strenge CORS-Regeln
  },
  database: {
    synchronize: false, // NIEMALS automatische Schema-Änderungen
    logging: false,
    ssl: {
      rejectUnauthorized: true,
    },
    pool: {
      min: 10,
      max: 50,
    },
  },
  security: {
    rateLimiting: {
      enabled: true,
      max: 100, // Strenge Limits
      windowMs: 15 * 60 * 1000,
    },
    helmet: {
      enabled: true,
      contentSecurityPolicy: true,
      hsts: true,
    },
  },
  email: {
    provider: 'sendgrid',
    useProductionCredentials: true,
  },
  cache: {
    ttl: 3600, // 1 Stunde für bessere Performance
  },
  features: {
    enableRegistration: true,
    enablePasswordReset: true,
    enableEmailVerification: true,
    enableTwoFactorAuth: true, // Alle Sicherheitsfeatures aktiv
  },
  monitoring: {
    enabled: true,
    errorReporting: true,
    performanceTracking: true,
  },
};

17.5.2 Environment Manager Service

// environment.service.ts - Zentraler Manager für Umgebungen
@Injectable()
export class EnvironmentService {
  private readonly environment: string;
  private readonly config: any;

  constructor(private configService: ConfigService) {
    this.environment = this.configService.get('NODE_ENV', 'development');
    this.loadEnvironmentConfig();
  }

  private loadEnvironmentConfig(): void {
    switch (this.environment) {
      case 'development':
        this.config = developmentConfig;
        break;
      case 'test':
        this.config = testConfig;
        break;
      case 'staging':
        this.config = stagingConfig;
        break;
      case 'production':
        this.config = productionConfig;
        break;
      default:
        throw new Error(`Unbekannte Umgebung: ${this.environment}`);
    }
  }

  // Environment-Checks
  get isDevelopment(): boolean {
    return this.environment === 'development';
  }

  get isTest(): boolean {
    return this.environment === 'test';
  }

  get isStaging(): boolean {
    return this.environment === 'staging';
  }

  get isProduction(): boolean {
    return this.environment === 'production';
  }

  get isProductionLike(): boolean {
    return this.isProduction || this.isStaging;
  }

  // Feature-Flags basierend auf Umgebung
  isFeatureEnabled(featureName: string): boolean {
    return this.config.features?.[featureName] ?? false;
  }

  // Umgebungsabhängige Konfiguration
  getDatabaseConfig(): any {
    const baseConfig = this.configService.get('database');
    return { ...baseConfig, ...this.config.database };
  }

  getSecurityConfig(): any {
    return this.config.security;
  }

  getAppConfig(): any {
    return this.config.app;
  }

  // Debugging und Monitoring
  shouldEnableSwagger(): boolean {
    return this.config.app?.enableSwagger ?? false;
  }

  shouldLogSqlQueries(): boolean {
    return this.config.database?.logging ?? false;
  }

  getLogLevel(): string {
    return this.config.app?.logLevel ?? 'info';
  }

  // Performance-Optimierungen basierend auf Umgebung
  getCacheTtl(): number {
    return this.config.cache?.ttl ?? 300;
  }

  shouldUseConnectionPooling(): boolean {
    return this.isProductionLike;
  }

  getMaxConnectionPoolSize(): number {
    return this.config.database?.pool?.max ?? 10;
  }

  // Fehlerbehandlung und Monitoring
  shouldReportErrors(): boolean {
    return this.config.monitoring?.errorReporting ?? this.isProductionLike;
  }

  shouldTrackPerformance(): boolean {
    return this.config.monitoring?.performanceTracking ?? this.isProductionLike;
  }

  // Hilfsmethoden für häufige Checks
  allowsAutoSchemaSync(): boolean {
    return !this.isProductionLike && this.config.database?.synchronize;
  }

  requiresStrictSecurity(): boolean {
    return this.isProductionLike;
  }

  allowsMockServices(): boolean {
    return this.isDevelopment || this.isTest;
  }
}

17.6 Secrets Management - Der Tresor für sensible Daten

Secrets Management ist wie ein Hochsicherheitstresor für die wertvollsten Dinge Ihrer Anwendung. Passwörter, API-Schlüssel, Datenbank-Credentials und andere sensible Informationen dürfen niemals im Quellcode stehen oder unverschlüsselt gespeichert werden. Sie brauchen besonderen Schutz.

17.6.1 Grundprinzipien des Secrets Management

Das wichtigste Prinzip ist: Secrets gehören niemals in den Code oder ins Version Control System. Sie sollten immer separat gespeichert und zur Laufzeit geladen werden. Es ist wie der Unterschied zwischen einem Foto Ihres Hausschlüssels (unsicher) und dem echten Schlüssel, den Sie sicher verwahren.

// secrets.service.ts - Sicherer Umgang mit Secrets
@Injectable()
export class SecretsService {
  private readonly secrets = new Map<string, string>();
  private readonly logger = new Logger(SecretsService.name);

  constructor(
    private configService: ConfigService,
    private environmentService: EnvironmentService,
  ) {
    this.loadSecrets();
  }

  private loadSecrets(): void {
    // Definiere welche Secrets erforderlich sind
    const requiredSecrets = [
      'JWT_SECRET',
      'DATABASE_PASSWORD',
      'ENCRYPTION_KEY',
    ];

    const optionalSecrets = [
      'SENDGRID_API_KEY',
      'STRIPE_SECRET_KEY',
      'GOOGLE_CLIENT_SECRET',
    ];

    // Lade erforderliche Secrets
    for (const secretName of requiredSecrets) {
      const value = this.loadSecret(secretName);
      if (!value) {
        throw new Error(`Erforderliches Secret nicht gefunden: ${secretName}`);
      }
      this.secrets.set(secretName, value);
    }

    // Lade optionale Secrets
    for (const secretName of optionalSecrets) {
      const value = this.loadSecret(secretName);
      if (value) {
        this.secrets.set(secretName, value);
      } else {
        this.logger.warn(`Optionales Secret nicht gefunden: ${secretName}`);
      }
    }

    this.validateSecrets();
  }

  private loadSecret(name: string): string | null {
    // 1. Versuche aus Environment Variables
    let value = process.env[name];
    if (value) {
      return value;
    }

    // 2. Versuche aus Dateien (für Docker Secrets)
    const secretFile = process.env[`${name}_FILE`];
    if (secretFile) {
      try {
        value = fs.readFileSync(secretFile, 'utf8').trim();
        return value;
      } catch (error) {
        this.logger.error(`Konnte Secret-Datei nicht lesen: ${secretFile}`);
      }
    }

    // 3. Versuche aus externem Secret Manager (AWS, Azure, etc.)
    if (this.environmentService.isProduction) {
      return this.loadFromExternalSecretManager(name);
    }

    return null;
  }

  private loadFromExternalSecretManager(name: string): string | null {
    // Diese Methode würde mit AWS Secrets Manager, Azure Key Vault,
    // HashiCorp Vault, etc. integrieren
    // Hier vereinfacht dargestellt
    
    try {
      // Beispiel für AWS Secrets Manager
      // const secret = await this.awsSecretsManager.getSecretValue(name);
      // return secret.SecretString;
      return null; // Placeholder
    } catch (error) {
      this.logger.error(`Fehler beim Laden von Secret ${name} aus externem Manager`);
      return null;
    }
  }

  private validateSecrets(): void {
    // Validiere JWT Secret
    const jwtSecret = this.getSecret('JWT_SECRET');
    if (jwtSecret.length < 32) {
      throw new Error('JWT_SECRET muss mindestens 32 Zeichen haben');
    }

    if (this.environmentService.isProduction && jwtSecret.length < 64) {
      throw new Error('JWT_SECRET muss in Produktion mindestens 64 Zeichen haben');
    }

    // Validiere Datenbank-Passwort
    const dbPassword = this.getSecret('DATABASE_PASSWORD');
    if (this.environmentService.isProduction && dbPassword.length < 12) {
      throw new Error('DATABASE_PASSWORD muss in Produktion mindestens 12 Zeichen haben');
    }

    // Prüfe auf schwache Secrets
    this.checkForWeakSecrets();
  }

  private checkForWeakSecrets(): void {
    const weakPatterns = [
      'password',
      '123456',
      'qwerty',
      'admin',
      'secret',
    ];

    for (const [name, value] of this.secrets.entries()) {
      const lowerValue = value.toLowerCase();
      for (const pattern of weakPatterns) {
        if (lowerValue.includes(pattern)) {
          if (this.environmentService.isProduction) {
            throw new Error(`Schwaches Secret gefunden: ${name}`);
          } else {
            this.logger.warn(`Schwaches Secret in Entwicklung: ${name}`);
          }
        }
      }
    }
  }

  // Sichere API zum Abrufen von Secrets
  getSecret(name: string): string {
    const value = this.secrets.get(name);
    if (!value) {
      throw new Error(`Secret nicht gefunden: ${name}`);
    }
    return value;
  }

  hasSecret(name: string): boolean {
    return this.secrets.has(name);
  }

  // Sichere API für optionale Secrets
  getOptionalSecret(name: string, defaultValue?: string): string | undefined {
    return this.secrets.get(name) || defaultValue;
  }

  // Secret-Rotation Support
  async rotateSecret(name: string, newValue: string): Promise<void> {
    // Validiere das neue Secret
    if (newValue.length < 32) {
      throw new Error('Neues Secret muss mindestens 32 Zeichen haben');
    }

    // Speichere das alte Secret für Rollback
    const oldValue = this.secrets.get(name);
    
    try {
      // Aktualisiere das Secret
      this.secrets.set(name, newValue);
      
      // Benachrichtige andere Services über die Änderung
      await this.notifySecretRotation(name);
      
      this.logger.log(`Secret erfolgreich rotiert: ${name}`);
    } catch (error) {
      // Rollback bei Fehler
      if (oldValue) {
        this.secrets.set(name, oldValue);
      }
      throw error;
    }
  }

  private async notifySecretRotation(secretName: string): Promise<void> {
    // Hier würden Sie andere Services benachrichtigen,
    // dass sich ein Secret geändert hat
    // z.B. JWT Service bei JWT_SECRET Rotation
  }

  // Masking für Logs
  maskSecret(secret: string): string {
    if (secret.length <= 8) {
      return '*'.repeat(secret.length);
    }
    
    const start = secret.substring(0, 2);
    const end = secret.substring(secret.length - 2);
    const middle = '*'.repeat(secret.length - 4);
    
    return `${start}${middle}${end}`;
  }

  // Audit-Funktionen
  getSecretNames(): string[] {
    return Array.from(this.secrets.keys());
  }

  logSecretStatus(): void {
    this.logger.log('Secret Status:');
    for (const name of this.getSecretNames()) {
      const value = this.getSecret(name);
      this.logger.log(`  ${name}: ${this.maskSecret(value)} (${value.length} Zeichen)`);
    }
  }
}

17.6.2 Docker Secrets Integration

Docker Secrets sind eine elegante Möglichkeit, sensible Daten an Container zu übergeben, ohne sie in Environment Variables zu speichern, die leichter kompromittiert werden können.

// docker-secrets.service.ts - Integration mit Docker Secrets
@Injectable()
export class DockerSecretsService {
  private readonly secretsPath = '/run/secrets';
  private readonly logger = new Logger(DockerSecretsService.name);

  async loadDockerSecret(secretName: string): Promise<string | null> {
    const secretPath = path.join(this.secretsPath, secretName);
    
    try {
      // Prüfe ob die Secret-Datei existiert
      await fs.access(secretPath, fs.constants.R_OK);
      
      // Lade den Inhalt der Secret-Datei
      const secretValue = await fs.readFile(secretPath, 'utf8');
      
      // Entferne Whitespace am Ende
      return secretValue.trim();
    } catch (error) {
      if (error.code === 'ENOENT') {
        // Datei existiert nicht - das ist OK für optionale Secrets
        return null;
      } else {
        // Anderer Fehler - loggen und weiterwerfen
        this.logger.error(`Fehler beim Laden von Docker Secret ${secretName}: ${error.message}`);
        throw error;
      }
    }
  }

  async loadAllDockerSecrets(): Promise<Map<string, string>> {
    const secrets = new Map<string, string>();

    try {
      // Liste alle Dateien im Secrets-Verzeichnis
      const secretFiles = await fs.readdir(this.secretsPath);
      
      for (const fileName of secretFiles) {
        try {
          const secretValue = await this.loadDockerSecret(fileName);
          if (secretValue) {
            secrets.set(fileName, secretValue);
          }
        } catch (error) {
          this.logger.error(`Konnte Docker Secret ${fileName} nicht laden: ${error.message}`);
        }
      }
    } catch (error) {
      if (error.code === 'ENOENT') {
        this.logger.debug('Kein Docker Secrets Verzeichnis gefunden');
      } else {
        this.logger.error(`Fehler beim Zugriff auf Docker Secrets: ${error.message}`);
      }
    }

    return secrets;
  }
}

// docker-compose.yml Beispiel für Secrets
/*
version: '3.8'

services:
  app:
    build: .
    secrets:
      - jwt_secret
      - database_password
      - stripe_secret_key
    environment:
      - NODE_ENV=production
      # Secrets werden als Dateien gemountet, nicht als Env-Vars

secrets:
  jwt_secret:
    file: ./secrets/jwt_secret.txt
  database_password:
    file: ./secrets/database_password.txt
  stripe_secret_key:
    external: true  # Externes Secret aus Docker Swarm
*/

Configuration Management ist das Rückgrat jeder professionellen Anwendung. Wie ein gut organisiertes Büro, wo jeder weiß, wo was steht und wie alles funktioniert, sorgt gutes Configuration Management dafür, dass Ihre Anwendung in jeder Umgebung reibungslos läuft. Die Zeit, die Sie in ein solides Configuration System investieren, zahlt sich vielfach aus durch einfachere Deployments, weniger Fehler und bessere Sicherheit.