Saltar al contenido

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.

4 min de lectura
Módulo de configuración en TypeScript validando variables de entorno al inicio

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.

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?

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.

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();

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.

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.",
        },
      ],
    },
  },
];

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.

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

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

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" },
  );

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.

EnfoqueCuándo usarloNivel de seguridad
Archivo .envSolo desarrollo localBajo — nunca en CI/prod
Variables de entorno de la plataforma (Vercel, Railway)Despliegues simplesMedio — encriptadas en reposo
Gestor de secretos (AWS SSM, Vault)Entornos regulados o de alta seguridadAlto — auditoría, rotación
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 };
}

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

  1. Valida todas las variables de entorno al arrancar — falla temprano con un error claro, no tarde con uno críptico
  2. Centraliza la configuración en un único módulo tipado — prohíbe el acceso directo a process.env
  3. Usa Zod o similar para validación en runtime con inferencia de tipos automática
  4. Commitea .env.example, nunca .env — los nuevos desarrolladores necesitan documentación, no tus secretos
  5. Usa gestores de secretos para credenciales de producción — las variables de entorno solas no son suficientes para entornos regulados
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX