Adiós a process.env.UNDEFINED: variables de entorno con seguridad de tipos
1. La «trampa» de process.env
En una aplicación Node.js accedemos a las variables de entorno mediante process.env. Es un mecanismo sencillo y eficaz, pero tiene un problema inherente: no ofrece seguridad de tipos.
Los valores del objeto process.env son string o undefined. Esto provoca varios problemas habituales:
- Un
undefinedinesperado: si olvidas definir una variable en el archivo.envo en el servidor,process.env.MY_VARseráundefineden tiempo de ejecución. Esto puede causar unTypeErroren una parte profunda de la lógica. - Tipos que no coinciden: quizá esperes que un número de puerto sea de tipo
number, peroprocess.env.PORTsiempre es una cadena y tienes que llamar manualmente aparseInten cada lugar donde lo utilizas. - Lógica de validación dispersa: solemos escribir código defensivo en distintos rincones, como
const port = process.env.PORT || 3000;, lo que desordena la gestión de las variables de entorno.
2. Principio fundamental: fallar cuanto antes
Para una configuración crítica como las variables de entorno, una buena práctica es fallar cuanto antes (Fail-Fast).
Esto significa que la aplicación debe comprobar al comienzo del arranque si se han proporcionado todas las variables obligatorias y si su formato es correcto. Ante cualquier problema, debe lanzar inmediatamente un error y detenerse, en lugar de fallar más adelante, en un momento imprevisible, debido a una configuración incorrecta.
Así descubrimos los problemas de configuración de inmediato durante el despliegue o el desarrollo, y no cuando una persona accede a la aplicación.
3. Validación con seguridad de tipos mediante Zod
Zod es una biblioteca de declaración y validación de esquemas diseñada primero para TypeScript. Resulta especialmente adecuada para resolver el problema de seguridad de tipos de las variables de entorno.
Nuestra estrategia es:
- Definir un esquema Zod para todas las variables de entorno.
- Analizar
process.envcon ese esquema al arrancar la aplicación. - Si el análisis falla —es decir, si no pasa la validación—, Zod lanza un error y el arranque de la aplicación falla.
- Si el análisis tiene éxito, obtenemos un objeto completamente tipado que utilizamos en toda la aplicación.
Ejemplo de implementación
Primero, instala Zod: pnpm add zod
Después, crea un archivo dedicado a las variables de entorno, por ejemplo src/env.ts:
// src/env.ts
import { z } from 'zod';
// 1. 定义 Schema
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required.'),
PORT: z.coerce.number().int().positive().default(3000),
// z.coerce 会尝试将字符串转换为数字
});
// 2. 解析和导出
// .parse 会在验证失败时抛出错误,实现 "Fail-Fast"
export const env = envSchema.parse(process.env);Ahora puedes importar desde src/env el objeto env en cualquier otra parte de la aplicación.
// src/server.ts
import { env } from './env'; // 导入经过验证和类型化的 env 对象
// env.PORT 的类型是 `number`,而不是 `string | undefined`
const port = env.PORT;
// env.NODE_ENV 的类型是 'development' | 'production' | 'test'
if (env.NODE_ENV === 'development') {
console.log('Running in development mode');
}
// 如果 DATABASE_URL 未设置,应用在启动时就已经崩溃了,
// 所以在这里我们可以放心地认为它是存在的,并且类型是 `string`。
connectToDatabase(env.DATABASE_URL);4. Ventajas
Este patrón aporta beneficios inmediatos:
- Seguridad de tipos completa: cuando accedes a
env.PORTen el código, TypeScript sabe que es unnumber. - Documentación y validación centralizadas: el propio archivo
env.tsse convierte en la documentación oficial de las variables de entorno y reúne toda la lógica de validación. - Ejecución fiable: evita errores en tiempo de ejecución provocados por variables de entorno mal escritas, ausentes o con formato incorrecto.
- Fallo temprano: cualquier problema de configuración se descubre inmediatamente durante el despliegue o el desarrollo.
Conclusión
Adelantar la validación de las variables de entorno al arranque de la aplicación y utilizar una herramienta como Zod para garantizar la seguridad de tipos es una práctica de ingeniería con una excelente relación entre esfuerzo y beneficio. Mejora notablemente la robustez de la aplicación y la confianza de quienes la desarrollan, y constituye una parte imprescindible de un proyecto TypeScript moderno.