astro:env: un esquema tipado para tus variables
La evolución de Astro sobre import.meta.env: declarar las variables de entorno en un esquema con envField. Cómo se define en astro.config.mjs, qué significan los dos ejes context cliente o servidor y access público o secreto, por qué la esquina cliente más secreto es imposible por diseño, y cómo los tipos string, number, boolean y enum convierten el texto crudo en valores validados.
import.meta.env funciona, pero deja todo el trabajo pesado en tus manos: recordar qué variables existen, convertir el texto a números, comprobar que no falte ninguna y confiar en el prefijo para no filtrar secretos. astro:env es la respuesta madura de Astro a ese desorden: un esquema donde declaras, una sola vez y en un único lugar, cada variable que tu proyecto necesita —su tipo, si es pública o secreta, y si vive en el cliente o en el servidor—. A partir de esa declaración, Astro genera módulos tipados para leerla, valida su presencia antes de arrancar y hace imposibles por construcción las combinaciones peligrosas. Dejas de tener variables sueltas y pasas a tener un contrato.
- Declarar variables en la clave
env.schemadeastro.config.mjscon el ayudanteenvField. - Distinguir el eje
context(clientoserver) del ejeaccess(publicosecret). - Entender por qué la combinación cliente más secreto está prohibida por diseño.
- Elegir el tipo adecuado entre
string,number,booleanyenumcon sus validadores.
Un esquema declara tus variables una sola vez
En lugar de esparcir accesos a import.meta.env por todo el proyecto y esperar que las variables existan, con astro:env declaras la lista completa en la configuración. Cada entrada asigna un nombre a una descripción construida con envField, que se importa junto a defineConfig.
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
PUBLIC_API_URL: envField.string({ context: 'client', access: 'public' }),
PORT: envField.number({ context: 'server', access: 'public', default: 8787 }),
API_SECRET: envField.string({ context: 'server', access: 'secret' }),
},
},
});
Esta declaración es la única fuente de verdad. De ella Astro deriva dos cosas a la vez: el conocimiento para validar tu entorno —comprobar que PORT es un número y que API_SECRET está presente— y los tipos que verás al leer cada variable. Ya no hay nombres mágicos dispersos por el código; hay un catálogo central que el compilador conoce y hace respetar.
Declarar API_SECRET en el esquema no cambia de dónde sale su valor: sigue viniendo de un archivo .env, del panel de tu plataforma o del entorno del proceso. Lo que añade el esquema es una capa de interpretación entre esa fuente cruda y tu código. La variable entra como texto, el esquema la valida y la convierte, y tú la recibes ya tipada. El prefijo PUBLIC_ deja de ser obligatorio como marca de seguridad, porque ahora es el par context y access quien decide el destino de cada variable con mucha más precisión.
Los dos ejes: context y access
Cada campo del esquema se sitúa en dos ejes independientes, y ahí reside toda la potencia del sistema. El eje context responde a dónde se puede leer la variable: client significa que su valor viajará al navegador; server que solo existe del lado del servidor. El eje access responde a qué naturaleza tiene: public es un valor que no importa revelar; secret es una credencial que debe permanecer oculta.
envField.string({ context: 'client', access: 'public' }); // llega al navegador, inlineada
envField.string({ context: 'server', access: 'public' }); // solo servidor, conocida en build
envField.string({ context: 'server', access: 'secret' }); // solo servidor, leida en runtime
Las tres combinaciones anteriores son legítimas y cubren casos reales distintos. Una URL de API pública se declara client y public: se incrusta en el bundle como haría una variable PUBLIC_. Un puerto de escucha se declara server y public: no es secreto, pero tampoco tiene sentido en el navegador. Una clave de firma se declara server y secret: solo el servidor la ve y, además, se lee en el momento de la petición y no se hornea en ningún artefacto.
Falta una cuarta combinación, y su ausencia es el corazón del diseño. context: 'client' con access: 'secret' es una contradicción: un secreto que viaja al navegador ha dejado de ser secreto en el instante en que se empaqueta. Astro no confía en que recuerdes esta regla; la impone. Si intentas declarar un campo con esa pareja, la configuración es inválida y el proyecto no arranca. No es un aviso que puedas ignorar ni una buena práctica opcional: es un estado que el sistema de tipos vuelve irrepresentable, de modo que el error más caro —publicar una credencial— nunca llega a compilar.
Tipos y validadores de envField
envField no solo ubica la variable en los dos ejes: también le asigna un tipo, y con él una conversión y una validación. El texto crudo del entorno se transforma en el valor que tu código espera, y si no encaja, el fallo aparece pronto y con un mensaje claro en vez de propagarse como un NaN silencioso.
string
Cadenas con validadores opcionales como max, min, length, startsWith o una lista de patrones. El tipo mas comun.
number
Convierte el texto a numero y admite min y max. Perfecto para puertos, tiempos de espera o limites.
boolean
Interpreta el texto como verdadero o falso, para banderas que activan o apagan una funcion.
enum
Restringe el valor a un conjunto cerrado, como development, staging o production. Rechaza cualquier otro.
Cada tipo acepta además tres opciones transversales que afinan el contrato. Con optional: true marcas que la variable puede faltar, y su ausencia deja de ser un error. Con default das un valor de reserva que se usa cuando el entorno no la aporta, lo que a menudo hace innecesario el optional. Y los validadores propios de cada tipo —un max en una cadena, un rango en un número, un enum cerrado— convierten reglas que antes vivían en comentarios o en tu memoria en condiciones que la máquina comprueba por ti.
env: {
schema: {
LOG_LEVEL: envField.enum({
context: 'server', access: 'public',
values: ['debug', 'info', 'warn', 'error'], default: 'info',
}),
MAX_UPLOAD_MB: envField.number({ context: 'server', access: 'public', default: 10 }),
},
}
Los validadores no son un adorno: cada regla que mueves del comentario al esquema es una comprobación que la máquina hace por ti en cada arranque. Un enum cerrado evita el error tipográfico que dejaría tu nivel de log en un valor que ninguna rama contempla; un rango numérico atrapa el puerto imposible antes de que el servidor intente escuchar en él. La validación deja de ser un acto de fe y pasa a ser una propiedad del sistema.
Si vienes de validar datos con Zod en tus colecciones de contenido, envField te resultará familiar, pero no es lo mismo. Es un conjunto reducido y especializado en lo que una variable de entorno puede ser —cadenas, números, booleanos y enumeraciones—, porque el entorno solo entrega texto y el abanico de formas útiles es pequeño. Esa limitación es deliberada: un esquema de entorno no necesita objetos anidados ni uniones complejas, y mantenerlo simple hace que su validación sea predecible y su mensaje de error, claro.
flowchart TD
F[campo del esquema] --> CTX{context}
CTX -->|client| CL[llega al navegador]
CTX -->|server| SR[solo en el servidor]
F --> ACC{access}
ACC -->|public| PU[valor revelable]
ACC -->|secret| SE[credencial oculta]
CL --> X[cliente mas secreto: prohibido]
SE --> X
style X fill:#f38ba8,color:#11111b
style CL fill:#f9e2af,color:#11111b
style SR fill:#89b4fa,color:#11111bQué cambia respecto a import.meta.env
Conviene situar astro:env frente al mecanismo de la lección anterior, porque conviven y no siempre es obvio cuándo preferir cada uno. import.meta.env sigue existiendo y es perfecto para las banderas del build —MODE, DEV, PROD— y para leer alguna variable sin ceremonia en un proyecto pequeño. astro:env gana en cuanto el proyecto crece y quieres garantías: tipos reales en vez de cadenas, validación de presencia y esa frontera cliente/servidor convertida en algo que el compilador impone.
// Antes: cadena sin garantias, conversion y comprobacion a mano
const puerto = Number(import.meta.env.PORT ?? 8787);
// Ahora: numero validado por el esquema, sin conversion ni valor magico disperso
import { PORT } from 'astro:env/server';
La diferencia no es de comodidad, sino de dónde recae la responsabilidad. Con import.meta.env, cada lugar que lee una variable debe recordar convertirla y defenderse de su ausencia; el conocimiento sobre su forma vive disperso y repetido, y basta que un solo sitio lo olvide para abrir un agujero. Con el esquema, ese conocimiento se declara una vez y se hace cumplir en todas partes: la variable entra al programa por un único portón vigilado en lugar de por mil puertas traseras que cada quien asegura como puede.
Adoptar astro:env no obliga a abandonar import.meta.env el mismo día. Puedes empezar por las variables que más te importan —los secretos y las que causan fallos por su tipo— declarándolas en el esquema, y dejar el resto como estaba. Ambos sistemas leen de los mismos archivos .env, así que la transición es gradual: cada variable que asciendes al esquema gana tipos y validación sin que las demás se enteren.
Es fácil confundir las dos opciones que ablandan una variable. optional: true dice que puede faltar, y entonces su tipo incluye la ausencia y tu código debe contemplarla. default dice qué usar cuando falta, y entonces la variable nunca llega vacía a tu código porque el esquema rellena el hueco. Casi siempre quieres default: convierte una variable que podría no estar en una que siempre tiene un valor sensato, y te ahorra comprobar su presencia en cada lectura.
No hace falta declarar cincuenta variables para que astro:env valga la pena. Con dos o tres —una URL pública, un puerto, un secreto— ya obtienes tipos, validación y la frontera impuesta por el compilador. Empieza pequeño y haz crecer el esquema al ritmo del proyecto: cada variable que añades queda documentada, tipada y vigilada desde el instante en que entra, sin deuda que saldar después. El coste de adoptarlo es casi nulo y el beneficio se cobra en cada fallo que deja de ocurrir en producción.
Lo que astro:env hace, sin anunciarlo con esas palabras, es aplicar a la configuración dos principios que la ingeniería seria aprendió por las malas. El primero es analizar en vez de validar. Validar es preguntarle a un dato si cumple una regla y seguir tratándolo como el texto informe que era; analizar es transformarlo, en la frontera, en un valor de un tipo que ya lleva la garantía dentro. Cuando declaras PORT como number con un rango, la variable deja de ser una cadena que podría ser cualquier cosa y se convierte, al cruzar el borde de tu programa, en un número que el resto del código puede usar sin volver a dudar. La duda se paga una vez, en un solo punto, y no en cada lectura. El segundo principio es hacer irrepresentables los estados ilegales. La combinación cliente más secreto no se documenta como peligrosa para que la evites con disciplina: se elimina del espacio de lo declarable, de modo que la disciplina sobra. Y aquí está la idea que trasciende Astro. La mayoría de los sistemas colocan sus controles tarde y por fuera —un guardia en ejecución, una revisión en la lista de tareas, una advertencia en la documentación— y confían en la vigilancia. Los sistemas mejores mueven el control temprano y hacia dentro, hasta el tipo mismo, donde el compilador lo custodia sin cansarse ni distraerse. Un esquema de entorno parece un lujo de comodidad —autocompletado, mensajes bonitos— hasta que reparas en lo que de verdad compra: transforma una clase entera de errores de operación, los que aparecen en producción a las tres de la madrugada porque faltaba una variable o sobraba un secreto, en errores de compilación que ni siquiera consiguen salir de tu máquina. Esa es la mudanza que separa configurar de contratar.
- En
astro.config.mjs, declara un esquema con una variableclientypublic, otraserverypubliccondefault, y una terceraserverysecret. - Intenta declarar una variable con
context: 'client'yaccess: 'secret'y lee el error con el que Astro rechaza arrancar. - Convierte una variable numérica con
envField.numbery comprueba que un valor no numérico en el.envproduce un fallo temprano y explícito. - Añade un
envField.enumpara el nivel de log con un valor por defecto y verifica que un valor fuera de la lista se rechaza.