Tema
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:
| Variable | Desarrollo (tu PC) | Producción (servidor) |
|---|---|---|
NODE_ENV | development | production |
PORT | 3000 | 8080 |
DATABASE_URL | mysql://root@localhost/tienda_dev | mysql://usuario:****@db.servidor.com/tienda |
JWT_SECRET | cualquier texto | una 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.jspowershell
# PowerShell
$env:PORT = "4000"; node src/index.jsCon 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:5173Opció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.jsjson
{
"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 ./.envOpción 2: el paquete dotenv
Muy común en proyectos existentes:
bash
npm i dotenvjs
// 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:5173txt
node_modules/
.env
.env.*
!.env.exampleSi falta JWT_SECRET verás al arrancar:
txt
❌ Variables de entorno inválidas:
- JWT_SECRET: Invalid input: expected string, received undefinedGenerar 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.exampleactualizado: 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
- Agrega al schema una variable
LOG_LEVELcon valoresdebug | info | warn | error(por defectoinfo) y una variable booleanaMAINTENANCE_MODEque acepte"true"/"false". - Crea un middleware que, si
MAINTENANCE_MODEes verdadero, responda503a 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
.envconnode --env-file=.env(nativo) odotenv. - Valida las variables con zod al arrancar y exporta un objeto
envtipado. .enven.gitignore;.env.examplesí se sube.