Zum Inhalt springen

Umgebungsvariablen richtig verwalten

Hör auf, process.env-Aufrufe über deine Codebasis zu verstreuen — validiere, typisiere und zentralisiere die Umgebungskonfiguration für sicherere Deployments.

3 Min. Lesezeit
TypeScript-Konfigurationsmodul, das Umgebungsvariablen beim Start validiert

Umgebungsvariablen sind der Standardweg, um Anwendungen über verschiedene Umgebungen hinweg zu konfigurieren. Das Problem ist nicht das Konzept — es ist, wie die meisten Codebasen sie nutzen. Verstreute process.env-Aufrufe, fehlende Validierung und stille Laufzeitfehler, wenn eine Variable undefined ist. Eine einzelne fehlende DATABASE_URL in Produktion sollte keinen kryptischen Fehler drei Minuten nach dem Start verursachen.

Das Problem des verstreuten Zugriffs

Wenn auf Umgebungsvariablen direkt dort zugegriffen wird, wo sie gebraucht werden, entsteht undefiniertes Verhalten, das über die gesamte Codebasis verstreut ist.

tstypescript
// ❌ Direct access — no validation, no types, no single source of truth
// In database.ts
const client = new PrismaClient({
  datasources: { db: { url: process.env.DATABASE_URL } },
});
 
// In email.ts
const apiKey = process.env.SENDGRID_API_KEY; // undefined in staging?
 
// In auth.ts
const secret = process.env.JWT_SECRET!; // non-null assertion hiding a bug
const expiresIn = process.env.JWT_EXPIRES_IN || "1h"; // is "1h" correct for prod?

Fehlt SENDGRID_API_KEY, merkt man das erst, wenn jemand eine E-Mail auslöst — möglicherweise Stunden nach dem Deployment.

Beim Start validieren

Lade und validiere alle Umgebungsvariablen einmal, beim Start der Anwendung. Fehlt etwas oder ist es ungültig, stürze sofort mit einer klaren Fehlermeldung ab.

tstypescript
// ✅ Centralized, validated, typed configuration
import { z } from "zod";
 
const envSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default("24h"),
  SENDGRID_API_KEY: z.string().startsWith("SG."),
  ALLOWED_ORIGINS: z
    .string()
    .transform((s) => s.split(","))
    .pipe(z.array(z.string().url())),
});
 
export type Env = z.infer<typeof envSchema>;
 
function loadEnv(): Env {
  const result = envSchema.safeParse(process.env);
 
  if (!result.success) {
    console.error("Invalid environment variables:");
    console.error(result.error.flatten().fieldErrors);
    process.exit(1);
  }
 
  return result.data;
}
 
export const env = loadEnv();

Jetzt erhält jeder Import von env typisierte, validierte Werte. Fehlende Variablen bringen die App beim Start zum Absturz — nicht um 3 Uhr nachts, wenn ein Nutzer einen Codepfad auslöst.

Typsicherheit durchgängig

Sobald du ein validiertes env-Objekt hast, nutze es überall statt process.env.

tstypescript
// ❌ Type-unsafe — process.env values are always string | undefined
const port = parseInt(process.env.PORT || "3000"); // manual parsing
const isProduction = process.env.NODE_ENV === "production"; // string comparison
 
// ✅ Type-safe — env.PORT is already a number, env.NODE_ENV is a union type
import { env } from "./config";
 
const port = env.PORT; // number
const isProduction = env.NODE_ENV === "production"; // TypeScript narrows correctly
tstypescript
// Lint rule to ban direct process.env access
// eslint.config.mjs
export default [
  {
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector:
            "MemberExpression[object.name='process'][property.name='env']",
          message: "Use the validated `env` object from ./config instead.",
        },
      ],
    },
  },
];

.env-Dateien für die Entwicklung

Nutze .env-Dateien für die lokale Entwicklung, niemals in Produktion. Produktionsumgebungen sollten Variablen über die Deployment-Plattform injizieren.

shbash
# .env.example — committed to git, documents required variables
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
JWT_SECRET=dev-secret-at-least-32-characters-long
SENDGRID_API_KEY=SG.dev-key
ALLOWED_ORIGINS=http://localhost:3000
shbash
# .env — git-ignored, contains real local values
# .env.production — NEVER exists. Production vars come from the platform.
shbash
# .gitignore
.env
.env.local
.env.*.local

Committe immer eine .env.example mit Platzhalterwerten. Neue Entwickler sollten cp .env.example .env ausführen, echte Werte eintragen und direkt loslegen können.

Umgebungsspezifische Konfiguration

Manche Werte ändern sich zwischen Umgebungen. Behandle das in der Validierungsschicht, nicht verstreut in der Business-Logik.

tstypescript
const envSchema = z
  .object({
    NODE_ENV: z.enum(["development", "production", "test"]),
    DATABASE_URL: z.string().url(),
    LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
    ENABLE_EMAIL: z.coerce.boolean().default(true),
    SENTRY_DSN: z.string().url().optional(),
  })
  .refine(
    (env) => {
      // Sentry DSN required in production
      if (env.NODE_ENV === "production" && !env.SENTRY_DSN) return false;
      return true;
    },
    { message: "SENTRY_DSN is required in production" },
  );

Das macht die Anforderungen pro Umgebung explizit. Ein Staging-Deployment ohne SENTRY_DSN ist in Ordnung; ein Produktions-Deployment ohne sie schlägt beim Start fehl.

Secrets-Management

Umgebungsvariablen funktionieren für Konfiguration, aber Secrets verdienen zusätzlichen Schutz.

AnsatzWann verwendenSicherheitsniveau
.env-DateiNur lokale EntwicklungNiedrig — niemals in CI/Prod
Plattform-Umgebungsvariablen (Vercel, Railway)Einfache DeploymentsMittel — verschlüsselt gespeichert
Secrets-Manager (AWS SSM, Vault)Regulierte oder hochsichere UmgebungenHoch — Audit-Trails, Rotation
tstypescript
// For secrets managers, load at startup alongside env vars
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
 
async function loadSecrets() {
  const ssm = new SSMClient({});
 
  const dbPassword = await ssm.send(
    new GetParameterCommand({
      Name: "/myapp/prod/database-password",
      WithDecryption: true,
    }),
  );
 
  return { DATABASE_PASSWORD: dbPassword.Parameter?.Value };
}

Committe niemals Secrets in git. Selbst in privaten Repositories bleiben Secrets in der Git-Historie über Force-Pushes und gelöschte Branches hinweg erhalten.

Die wichtigsten Punkte

  1. Validiere alle Umgebungsvariablen beim Start — stürze früh mit einem klaren Fehler ab, nicht spät mit einem kryptischen
  2. Zentralisiere die Konfiguration in einem einzigen typisierten Modul — verbiete direkten process.env-Zugriff
  3. Nutze Zod oder Ähnliches für Laufzeitvalidierung mit automatischer Typinferenz
  4. Committe .env.example, niemals .env — neue Entwickler brauchen Dokumentation, nicht deine Secrets
  5. Nutze Secrets-Manager für Produktions-Credentials — Umgebungsvariablen allein reichen für regulierte Umgebungen nicht aus
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX