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