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.
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.
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
}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.
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;
}.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=3600Verschiedene 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,
};
};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.
// 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 {}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);
}
}// 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 });
}
}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.
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;
});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
};
}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.
// 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,
},
};// 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;
}
}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.
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)`);
}
}
}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.