Typed Environment Variables

Validate process.env Once

process.env values are string | undefined. Read and validate them once at startup into a typed, frozen object; the app should refuse to start with bad configuration rather than fail on the first request.

Example

type Env = Readonly<{
  NODE_ENV: "development" | "production" | "test";
  PORT: number;
  DATABASE_URL: string;
  JWT_SECRET: string;
  CORS_ORIGINS: string[];
}>;

function loadEnv(source: Record<string, string | undefined>): Env {
  const errors: string[] = [];
  const req = (k: string): string => {
    const v = source[k];
    if (!v) errors.push(`${k} is required`);
    return v ?? "";
  };
  const nodeEnv = source.NODE_ENV ?? "development";
  if (nodeEnv !== "development" && nodeEnv !== "production" && nodeEnv !== "test") errors.push("NODE_ENV is invalid");
  const port = Number(source.PORT ?? "3000");
  if (!Number.isInteger(port) || port <= 0) errors.push("PORT must be a positive integer");
  const secret = req("JWT_SECRET");
  if (secret && secret.length < 32) errors.push("JWT_SECRET must be at least 32 characters");
  const env = {
    NODE_ENV: nodeEnv as Env["NODE_ENV"],
    PORT: port,
    DATABASE_URL: req("DATABASE_URL"),
    JWT_SECRET: secret,
    CORS_ORIGINS: (source.CORS_ORIGINS ?? "").split(",").filter(Boolean),
  };
  if (errors.length) throw new Error("Invalid environment:\n - " + errors.join("\n - "));
  return Object.freeze(env);
}

const env = loadEnv({ PORT: "8080", DATABASE_URL: "postgres://db/shop", JWT_SECRET: "x".repeat(40), CORS_ORIGINS: "https://shopease.in,https://admin.shopease.in" });
console.log(env.NODE_ENV, env.PORT + 1, env.CORS_ORIGINS.length);

try {
  loadEnv({ PORT: "abc", JWT_SECRET: "short" });
} catch (e) {
  console.log((e as Error).message);
}

Output

development 8081 2
Invalid environment:
 - PORT must be a positive integer
 - JWT_SECRET must be at least 32 characters
 - DATABASE_URL is required

Tip: In real projects use z.object({...}).parse(process.env) with Zod, or @t3-oss/env-nextjs in Next.js, which also separates server and NEXT_PUBLIC_ client variables.