wandres.dev
VARIABLES DE ENTORNO · astro:env

Leer variables: astro:env/client, /server y getSecret

Cómo se consume el esquema de astro:env desde el código: los módulos virtuales astro:env/client y astro:env/server, la importación por nombre ya tipada, la función getSecret para leer secretos en runtime cuando su valor solo existe en la plataforma, y cuándo valida Astro cada variable, en el build para las públicas y en ejecución para los secretos salvo que actives validateSecrets.

⏱ 15 min

Declarar el esquema es la mitad del contrato; la otra mitad es leerlo. astro:env no te devuelve las variables por el viejo import.meta.env, sino por dos módulos virtuales que Astro fabrica a partir de tu esquema: astro:env/client y astro:env/server. La separación no es cosmética. Cada módulo expone solo lo que su contexto puede ver, de modo que la frontera cliente/servidor que declaraste deja de ser una promesa y se vuelve una barrera que el propio sistema de importaciones vigila. Y para los secretos que ni siquiera existen cuando compilas —porque solo aparecen en la plataforma de despliegue— hay una puerta especial: getSecret.

🎯 Al terminar esta lección sabrás
  • Importar variables públicas ya tipadas desde astro:env/client y astro:env/server.
  • Entender por qué cada módulo solo expone las variables de su contexto.
  • Usar getSecret para leer secretos cuyo valor solo se conoce en tiempo de ejecución.
  • Saber cuándo valida Astro cada variable: en el build o en ejecución, según su naturaleza.

Dos módulos, una frontera respetada

Una vez definido el esquema, cada variable se importa por su nombre desde el módulo que le corresponde. Las variables de context: 'client' viven en astro:env/client; las de context: 'server' —públicas y secretas— viven en astro:env/server. La importación no te da texto crudo: te da el valor ya convertido al tipo que declaraste.

// Cliente: solo ve las variables context client
import { PUBLIC_API_URL } from 'astro:env/client';

// Servidor: ve las variables server, publicas y secretas
import { PORT, API_SECRET } from 'astro:env/server';

Repara en lo que desaparece respecto a import.meta.env. No hay conversión manual: PORT llega como número porque el esquema lo declaró number. No hay nombres mágicos escritos como cadenas: importas identificadores reales que el editor autocompleta y renombra. Y no hay duda sobre la presencia: si una variable requerida falta, el fallo salta antes, no en la línea donde intentas usarla.

⚠️
Importar del módulo equivocado es un error, no un undefined

La barrera es estricta y esa es su virtud. Si intentas importar API_SECRET desde astro:env/client, no obtienes undefined ni un valor vacío que se cuele silenciosamente hasta producción: obtienes un error que impide compilar. El módulo de cliente sencillamente no exporta ese nombre, porque el esquema declaró la variable como de servidor. Con import.meta.env un secreto mal ubicado se degradaba a undefined y el problema aparecía tarde; aquí la frontera es una lista de exportaciones que no se puede burlar por descuido.

getSecret: leer lo que solo existe en runtime

La importación directa de un secreto asume que su valor puede conocerse cuando Astro construye el módulo. Pero muchos secretos no cumplen eso: la clave de una pasarela de pago no está en tu máquina ni en el build, solo aparece inyectada en el entorno del servidor de despliegue, en el instante en que atiende una petición. Para ese caso existe getSecret, una función que también se importa de astro:env/server y lee el valor por su nombre en tiempo de ejecución.

import { getSecret } from 'astro:env/server';

export const GET = () => {
  const clave = getSecret('PAYMENT_KEY'); // string | undefined, sin validar
  if (!clave) return new Response('config incompleta', { status: 500 });
  // usar la clave para firmar o autenticar...
};

La diferencia con la importación por nombre es profunda. El identificador importado es un valor validado y tipado que el esquema garantiza; getSecret es una lectura dinámica y en crudo que devuelve una cadena o undefined, sin pasar por la validación del esquema. Por eso getSecret sirve para dos cosas: leer un secreto que solo aparece en runtime, y leer una variable que ni siquiera está en el esquema. Es la vía de escape deliberada para lo que no se puede conocer de antemano, con el coste justo de que tú te encargas de comprobar que llegó.

ℹ️
getSecret respeta el adapter de tu plataforma

getSecret no lee siempre del mismo sitio: delega en el adapter para saber de dónde salen las variables en cada runtime. En un servidor Node lee del entorno del proceso; en un runtime del edge puede leer del objeto de entorno que la plataforma inyecta en cada petición. Esa indirección es justo lo que te permite escribir el mismo código de lectura sin importar dónde despliegues, porque el adapter traduce el concepto de secreto de runtime al mecanismo concreto de la plataforma que tengas debajo.

Un uso frecuente de getSecret que conviene destacar es leer variables que ni siquiera están en el esquema. A veces una biblioteca de terceros espera encontrar su clave en una variable con un nombre fijo que tú no declaraste; getSecret te deja alcanzarla sin forzarla dentro de tu contrato, reconociendo que no toda variable del entorno es tuya ni tiene por qué pasar por tu validación.

💡
El esquema para lo tuyo, getSecret para lo ajeno

Una división útil: declara en el esquema las variables que tu código posee y controla, y reserva getSecret para las que pertenecen a otra herramienta o solo existen en la plataforma. Así tu esquema sigue siendo un retrato fiel de la configuración que tú gobiernas, sin inflarse con nombres que no son tu responsabilidad, y getSecret queda como la puerta honesta hacia todo lo demás.

Cuándo se valida cada variable

El esquema promete validar, pero no todo se valida en el mismo momento, y la razón es puramente física: no puedes comprobar en el build un valor que aún no existe. Astro reparte la validación según la naturaleza de cada variable.

🌐

astro:env/client

Expone las variables client mas public. Se validan e inlinean en el build, como las clasicas PUBLIC.

🖥️

astro:env/server

Expone las variables server, publicas y secretas, ya tipadas. Tambien ofrece la funcion getSecret.

🗝️

getSecret

Lectura dinamica por nombre en runtime. Devuelve cadena o indefinido, sin validar contra el esquema.

⏱️

validateSecrets

Opcion que fuerza a validar tambien los secretos en el build, si su valor esta disponible entonces.

Las variables públicas —de cliente o de servidor— se validan en el build, porque su valor está presente cuando compilas y no hay motivo para posponer el chequeo. Los secretos, en cambio, se validan por defecto en tiempo de ejecución, cuando el módulo se carga en el servidor, precisamente porque su valor puede no existir todavía durante el build. Si sabes que tus secretos sí están disponibles al compilar y prefieres que su ausencia rompa el build en lugar de la primera petición, actívalo explícitamente.

// astro.config.mjs
export default defineConfig({
  env: {
    schema: { /* ... */ },
    validateSecrets: true, // valida tambien los secretos en el build
  },
});
flowchart LR
SCH[esquema en la config] --> GEN[Astro genera modulos virtuales]
GEN --> CLI[astro env client]
GEN --> SRV[astro env server]
CLI --> BUILD[validado en el build]
SRV --> BUILD
SRV --> GS[getSecret lee en runtime]
GS --> RT[validado o crudo en ejecucion]
style GEN fill:#89b4fa,color:#11111b
style BUILD fill:#a6e3a1,color:#11111b
style RT fill:#f9e2af,color:#11111b
💡
Elige la puerta por cuándo conoces el valor, no por costumbre

La regla práctica para decidir entre la importación por nombre y getSecret no es de gusto, es de tiempo. Si el valor existe cuando compilas y quieres que su ausencia sea un error de build, impórtalo por nombre y deja que el esquema lo valide. Si el valor solo aparece en la plataforma en cada petición, usa getSecret y valida su presencia tú mismo. Mezclar los dos criterios —querer validar en build algo que solo llega en runtime, o leer con getSecret algo perfectamente conocido de antemano— es la fuente de los enredos más comunes al migrar a astro:env.

Errores tempranos y tipos generados

El mayor beneficio diario del esquema no es la lectura tipada, sino lo que ocurre cuando algo va mal. Si una variable requerida falta, Astro no te deja avanzar con un undefined que reventará tres capas más adentro: detiene el arranque con un mensaje que nombra la variable ausente y el módulo que la esperaba. El fallo aparece donde se puede arreglar —la configuración del entorno— y no en la línea remota donde por fin se usaba.

# Fallo temprano al arrancar sin una variable requerida por el esquema
[astro:env] API_SECRET es requerida por el esquema pero no esta definida

Ese diagnóstico depende de que el esquema sea la única fuente de verdad. Como Astro conoce el nombre, el tipo y la obligatoriedad de cada variable, los comprueba todos de una vez al inicio y te ofrece, además, autocompletado real: al escribir el import desde astro:env/server, el editor propone exactamente los nombres declarados y ninguno inventado. Los tipos no los escribes tú; los genera Astro a partir del esquema y los mantiene sincronizados con él, de modo que renombrar una variable en la configuración se propaga a cada uso.

ℹ️
El esquema documenta el entorno mejor que un comentario

Un efecto secundario valioso es que el esquema se vuelve documentación ejecutable. Quien llega al proyecto no rastrea los .env ni pregunta qué variables hacen falta: las lee en un solo lugar, con su tipo y su naturaleza declarados. Y a diferencia de un comentario, esta documentación no puede quedar desfasada sin que se note, porque es la misma que la máquina usa para validar. Si miente, el proyecto deja de arrancar; esa es la clase de documentación en la que se puede confiar.

⚠️
getSecret no valida: la carga recae en ti

Recuerda la contrapartida de la puerta dinámica. Un secreto importado por nombre pasa por el esquema y llega garantizado; el mismo secreto leído con getSecret llega como cadena o undefined, sin que nadie compruebe su forma. Si eliges getSecret, hazte cargo tú de lo que el esquema haría: verifica que el valor existe y tiene sentido antes de usarlo, porque aquí no hay una red debajo que atrape su ausencia.

Merece la pena insistir en un punto práctico antes de la reflexión final: la elección del módulo la dicta el esquema, no tú. No decides en el import si una variable es de cliente o de servidor; esa decisión ya la tomaste al declararla, y el módulo del que puedes importarla es su consecuencia directa. Si alguna vez te sorprende no encontrar un nombre donde lo buscas, la respuesta no está en el import sino en el esquema: vuelve a él y revisa qué context le diste, porque ahí se decidió a qué lado de la frontera vive esa variable para siempre.

💡
Deja que el editor te guíe por los módulos correctos

Una forma rápida de aprender la frontera es dejar que la herramienta te la enseñe. Escribe el import desde astro:env/client y mira qué nombres te ofrece el autocompletado: solo verás las variables de cliente. Repite con astro:env/server y aparecerán las de servidor junto a getSecret. Esa lista, generada del esquema, es la frontera hecha visible, y consultarla es más fiable que recordar de memoria dónde pusiste cada variable.

Un módulo virtual es una frontera hecha de importaciones

La decisión más elegante de astro:env es que la seguridad no vive en una función que compruebas, sino en la forma de un módulo que importas. Piensa en lo que significa que astro:env/client sencillamente no exporte tus secretos. No hay una comprobación que decida, en cada acceso, si tienes permiso; hay una ausencia estructural. El secreto no está prohibido en el cliente: no está. Y como el sistema de módulos es el mismo que resuelve todas tus importaciones, esa frontera la vigila la herramienta más incansable que tienes, el resolvedor de módulos, que jamás se cansa ni se distrae ni hace una excepción por las prisas del viernes. Esto ilustra un patrón que reaparece en el buen diseño una y otra vez: convertir una regla de política en una propiedad de la estructura. Una política —no leas secretos en el cliente— depende de que alguien la recuerde y la respete; una estructura —el cliente no tiene de dónde leerlos— no depende de nadie. Y observa el segundo acierto: getSecret existe como puerta explícita y estrecha para lo que la estructura no puede conocer de antemano, los valores que solo el runtime posee. En vez de debilitar toda la frontera para acomodar ese caso, Astro abre una única salida nombrada, con su propio contrato —devuelve cadena o indefinido, tú validas— de modo que lo dinámico queda confinado y señalado en lugar de contaminarlo todo. Esa es la firma del diseño maduro: haz que lo seguro sea lo estructural y lo por defecto, y que lo dinámico sea una excepción visible y acotada, nunca la regla difusa que todo lo permite.

⚔️ Consume el esquema desde los dos lados
  1. Importa una variable pública desde astro:env/client en un <script> y comprueba que llega tipada, sin conversión manual.
  2. Importa una secreta desde astro:env/server en un endpoint y confirma que el mismo import desde el módulo de cliente no compila.
  3. Sustituye esa lectura por getSecret y razona qué cambia en el tipo devuelto y en quién valida la presencia.
  4. Activa validateSecrets en la configuración, quita un secreto del entorno y observa cómo el fallo se adelanta del runtime al build.