Gestión de variables de entorno hecha correctamente
Deja de esparcir llamadas a process.env por todo tu código — valida, tipa y centraliza la configuración de entorno para despliegues más seguros.

Las variables de entorno son la forma estándar de configurar aplicaciones entre distintos entornos. El problema no es el concepto — es cómo la mayoría de los códigos las usan. Llamadas a process.env esparcidas, validación faltante y fallos silenciosos en runtime cuando una variable es undefined. Que falte una sola DATABASE_URL en producción no debería causar un error críptico tres minutos después del arranque.
El problema del acceso disperso
Cuando las variables de entorno se acceden directamente donde se necesitan, terminas con comportamiento indefinido esparcido por todo el código.
// ❌ 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?Si falta SENDGRID_API_KEY, no te enteras hasta que alguien dispara un email — posiblemente horas después del despliegue.
Valida al arrancar
Carga y valida todas las variables de entorno una sola vez, al arrancar la aplicación. Si algo falta o es inválido, falla inmediatamente con un mensaje de error claro.
// ✅ 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();Ahora cada import de env obtiene valores tipados y validados. Las variables faltantes tumban la app al arrancar — no a las 3 AM cuando un usuario dispara una ruta de código.
Type safety de punta a punta
Una vez que tienes un objeto env validado, úsalo en todas partes en lugar de process.env.
// ❌ 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// 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.",
},
],
},
},
];Archivos .env para desarrollo
Usa archivos .env para desarrollo local, nunca en producción. Los entornos de producción deberían inyectar las variables a través de la plataforma de despliegue.
# .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# .env — git-ignored, contains real local values
# .env.production — NEVER exists. Production vars come from the platform.# .gitignore
.env
.env.local
.env.*.localSiempre commitea un .env.example con valores de ejemplo. Los nuevos desarrolladores deberían poder hacer cp .env.example .env, completar los valores reales y empezar a trabajar.
Configuración específica por entorno
Algunos valores cambian entre entornos. Maneja esto en la capa de validación, no esparcido por la lógica de negocio.
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" },
);Esto hace explícitos los requisitos por entorno. Un despliegue de staging sin SENTRY_DSN está bien; un despliegue de producción sin él falla al arrancar.
Gestión de secretos
Las variables de entorno funcionan bien para configuración, pero los secretos merecen protección adicional.
| Enfoque | Cuándo usarlo | Nivel de seguridad |
|---|---|---|
Archivo .env | Solo desarrollo local | Bajo — nunca en CI/prod |
| Variables de entorno de la plataforma (Vercel, Railway) | Despliegues simples | Medio — encriptadas en reposo |
| Gestor de secretos (AWS SSM, Vault) | Entornos regulados o de alta seguridad | Alto — auditoría, rotación |
// 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 };
}Nunca commitees secretos a git. Incluso en repositorios privados, los secretos en el historial de git persisten a través de force-pushes y eliminaciones de ramas.
Puntos clave
- Valida todas las variables de entorno al arrancar — falla temprano con un error claro, no tarde con uno críptico
- Centraliza la configuración en un único módulo tipado — prohíbe el acceso directo a
process.env - Usa Zod o similar para validación en runtime con inferencia de tipos automática
- Commitea
.env.example, nunca.env— los nuevos desarrolladores necesitan documentación, no tus secretos - Usa gestores de secretos para credenciales de producción — las variables de entorno solas no son suficientes para entornos regulados


