Skip to content

Variables de entorno ​

Contraseñas de bases de datos, claves de APIs, puertos... nunca deben escribirse en el código. En esta lección aprenderás a usar variables de entorno y archivos .env de forma segura, con la opción nativa de Node y validándolas con zod.

Concepto ​

Una variable de entorno es un valor que vive fuera de tu código, en el sistema donde corre la app. El mismo código se comporta distinto según el entorno:

VariableDesarrollo (tu PC)Producción (servidor)
NODE_ENVdevelopmentproduction
PORT30008080
DATABASE_URLmysql://root@localhost/tienda_devmysql://usuario:****@db.servidor.com/tienda
JWT_SECRETcualquier textouna clave larga y aleatoria

Analogía: tu código es una receta; las variables de entorno son los ingredientes de cada cocina. La receta no cambia, pero cada restaurante usa sus propios ingredientes.

En Node se leen con process.env:

js
console.log(process.env.PORT)     // siempre string o undefined
console.log(process.env.NODE_ENV)

Definir variables ​

Desde la terminal ​

bash
# Linux / macOS
PORT=4000 node src/index.js
powershell
# PowerShell
$env:PORT = "4000"; node src/index.js

Con un archivo .env (lo habitual) ​

env
# .env
NODE_ENV=development
PORT=3000
DATABASE_URL="postgresql://postgres:secreto@localhost:5432/tienda"
JWT_SECRET="cambia-esto-por-una-clave-larga"
CORS_ORIGIN=http://localhost:5173

Opción 1: nativa (Node 20.6+), sin dependencias ​

bash
node --env-file=.env src/index.js

# Si el archivo puede no existir (por ejemplo en producción):
node --env-file-if-exists=.env src/index.js
json
{
  "scripts": {
    "dev": "node --watch --env-file=.env src/index.js",
    "start": "node src/index.js"
  }
}

También puedes cargarlo desde código:

js
process.loadEnvFile() // lee ./.env

Opción 2: el paquete dotenv ​

Muy común en proyectos existentes:

bash
npm i dotenv
js
// primera línea de src/index.js
import 'dotenv/config'

Validar la configuración con zod ​

Si falta una variable, es mejor que la app no arranque con un error claro, a que falle a mitad de la noche en producción.

js
import { z } from 'zod'

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().default(3000),
  DATABASE_URL: z.url(),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET debe tener al menos 32 caracteres'),
  JWT_EXPIRES_IN: z.string().default('15m'),
  CORS_ORIGIN: z.string().default('http://localhost:5173')
})

const resultado = EnvSchema.safeParse(process.env)

if (!resultado.success) {
  console.error('❌ Variables de entorno inválidas:')
  for (const issue of resultado.error.issues) {
    console.error(`  - ${issue.path.join('.')}: ${issue.message}`)
  }
  process.exit(1)
}

// Objeto congelado y tipado: úsalo en toda la app en lugar de process.env
export const env = Object.freeze(resultado.data)
export const esProduccion = env.NODE_ENV === 'production'
js
import { env } from './config/env.js'
import app from './app.js'

app.listen(env.PORT, () => {
  console.log(`[${env.NODE_ENV}] API en http://localhost:${env.PORT}`)
})
env
# Copia este archivo como .env y llena los valores
NODE_ENV=development
PORT=3000
DATABASE_URL="postgresql://usuario:password@localhost:5432/mi_base"
JWT_SECRET=""
JWT_EXPIRES_IN=15m
CORS_ORIGIN=http://localhost:5173
txt
node_modules/
.env
.env.*
!.env.example

Si falta JWT_SECRET verás al arrancar:

txt
❌ Variables de entorno inválidas:
  - JWT_SECRET: Invalid input: expected string, received undefined

Generar secretos seguros ​

bash
node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))"

Copia el resultado en JWT_SECRET. Usa un secreto distinto en cada entorno.

Varios entornos ​

txt
.env              ← desarrollo local (no se sube)
.env.test         ← para tests (no se sube o con datos falsos)
.env.example      ← plantilla (sí se sube)
json
{
  "scripts": {
    "dev": "node --watch --env-file=.env src/index.js",
    "test": "node --env-file=.env.test node_modules/vitest/vitest.mjs run"
  }
}

En producción no se usa archivo .env subido a Git: las variables se configuran en el panel del servicio (Render, Railway, Vercel) o en el servidor (PM2 ecosystem.config.cjs, systemd). Lo verás en Despliegue.

Variables en el frontend (cuidado) ​

En React/Vite, las variables VITE_* se incrustan en el JavaScript público. Cualquiera puede verlas. En el frontend sólo pon datos públicos (URL de tu API); los secretos viven en el backend.

Buena práctica

  • Centraliza la configuración en un solo módulo (config/env.js) y valida al arrancar.
  • Mantén .env.example actualizado: es la documentación de qué necesita tu app.
  • Rota (cambia) cualquier secreto que se haya filtrado, aunque borres el commit.

Error grave

Subir .env a GitHub. Hay bots que escanean repositorios públicos buscando claves de AWS, Stripe o bases de datos en minutos. Si te pasa, cambia las claves de inmediato: borrar el archivo no basta, sigue en el historial de Git.

Error común

Olvidar que process.env siempre devuelve strings: process.env.DEBUG === true nunca es verdadero, y process.env.PORT + 1 da "30001". Convierte los tipos (zod con coerce lo hace por ti).

Ejercicios ​

  1. Agrega al schema una variable LOG_LEVEL con valores debug | info | warn | error (por defecto info) y una variable booleana MAINTENANCE_MODE que acepte "true"/"false".
  2. Crea un middleware que, si MAINTENANCE_MODE es verdadero, responda 503 a todas las rutas excepto /api/salud.
Ver solución
js
// src/config/env.js (fragmento)
const EnvSchema = z.object({
  // ...resto de variables
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
  // z.coerce.boolean() convertiría "false" en true (string no vacío). Mejor:
  MAINTENANCE_MODE: z
    .enum(['true', 'false'])
    .default('false')
    .transform((v) => v === 'true')
})
js
// src/middlewares/mantenimiento.js
import { env } from '../config/env.js'

export function mantenimiento(req, res, next) {
  if (env.MAINTENANCE_MODE && req.path !== '/api/salud') {
    return res
      .status(503)
      .set('Retry-After', '600')
      .json({ error: 'Estamos en mantenimiento, vuelve en unos minutos' })
  }
  next()
}

// src/app.js: regístralo al inicio
// app.use(mantenimiento)

Resumen ​

  • Los secretos y la configuración van en variables de entorno, nunca en el código.
  • Carga .env con node --env-file=.env (nativo) o dotenv.
  • Valida las variables con zod al arrancar y exporta un objeto env tipado.
  • .env en .gitignore; .env.example sí se sube.

Academia Web — aprende, copia y construye.