Por qué separar config del código#
Imagina que el Team Builder hace peticiones a una API de héroes:
// src/main.js — esto es lo que queremos evitar
// URL hardcodeada en el codigo
fetch('https://api.teambuilder.local/heroes')
.then(function (r) { return r.json(); })
.then(function (heroes) {
// el array de heroes
console.log(heroes);
});Este código tiene un problema: la URL está escrita directamente. Para desplegarlo en producción tendrías que cambiarla. Para que un compañero lo ejecute en su máquina también. Y si algún día la URL cambia, hay que buscarla por todo el código.
La solución es separarla. En lugar de escribir la URL en el código, la pones en un fichero de configuración externo. El código solo sabe que hay una variable llamada VITE_API_URL y la usa.
Hay dos razones para separar la configuración del código:
- Por entorno: la misma app puede apuntar a servidores distintos según dónde se ejecute (desarrollo local, staging, producción) sin recompilar.
- Por seguridad: los secretos (tokens, claves de API privadas) no deben estar en el código fuente que sube a git.
Los ficheros .env#
Vite lee ficheros de texto con el formato CLAVE=valor. Se llaman ficheros .env y hay varios según el uso:
team-builder/
├── .env ← valores por defecto, todos los entornos
├── .env.local ← tu configuracion personal, NO se versiona
├── .env.production ← valores especificos de produccion, se versiona (solo valores PUBLICOS)
└── .env.example ← plantilla publica del proyecto, se versiona
.env.productionva a git, así que solo debe contener valores públicos (como la URL del servidor de producción). Los secretos reales nunca van aquí: van en.env.production.local(ignorado por git) o, mejor, directamente en las variables de entorno del servidor.
La sintaxis dentro del fichero es simple:
# Las lineas que empiezan por # son comentarios; no se leen.
VITE_API_URL=https://api.teambuilder.local
VITE_APP_TITLE=Team BuilderSin comillas, sin punto y coma. Solo clave, igual y valor.
Vite carga los ficheros en orden de prioridad (el más específico gana). Lo importante ahora: los ficheros que terminan en .local son para tu máquina. No se suben a git y no se comparten. Cada desarrollador tiene los suyos.
En Vite: import.meta.env y el prefijo VITE_#
Una vez que tienes las variables en un .env.local, las lees en el código con import.meta.env. import.meta es un objeto especial que el propio Vite rellena al construir tu código (no es algo que declares tú); .env es la propiedad donde expone las variables de entorno.
// En cualquier fichero .js o .ts de src/
// coge el valor del .env.local
const API_URL = import.meta.env.VITE_API_URL;
// usa la variable, no la URL directamente
fetch(API_URL + '/heroes')
.then(function (r) { return r.json(); })
.then(function (heroes) {
// el array de heroes del Team Builder
console.log(heroes);
});import.meta.env es el objeto donde Vite expone las variables. Pero no expone todas: solo las que empiezan por VITE_. El resto (sin prefijo) Vite las ignora al construir el bundle del cliente.
¿Por qué? Para que las variables del sistema operativo o del servidor (como DATABASE_URL o NODE_ENV) no acaben en el código que descarga el navegador por accidente.
Además de las variables que defines tú, Vite inyecta cuatro built-ins automáticamente:
// 'development' con npm run dev; 'production' con build
import.meta.env.MODE
// true cuando estas en desarrollo, false en produccion
import.meta.env.DEV
// true en produccion, false en desarrollo
import.meta.env.PROD
// la ruta base de la app (normalmente '/')
import.meta.env.BASE_URLEstas cuatro las puedes usar siempre, sin declararlas en ningún fichero .env:
// Mostrar informacion de depuracion solo en desarrollo
// solo true con npm run dev
if (import.meta.env.DEV) {
console.log('Dev: conectando a ' + import.meta.env.VITE_API_URL);
}.gitignore y .env.example#
Los ficheros .env*.local contienen los valores reales de tu máquina: URLs de servidores locales, tokens de desarrollo, configuraciones personales. No deben subirse a git.
El .gitignore estándar de Vite ya los excluye:
# .gitignore
.env*.localUn solo patrón protege todos: .env.local, .env.production.local, etc.
Pero si nadie sube los .env.local, ¿cómo sabe un colaborador nuevo qué variables necesita el proyecto? Ahí entra el fichero .env.example:
# .env.example — este fichero SI va a git
# Copia esto a .env.local y rellena los valores reales.
VITE_API_URL=https://api.ejemplo.comEl .env.example se versiona, se lee, y actúa como documentación viva. Lista las claves que el proyecto necesita sin revelar ningún valor sensible. Es el contrato entre el código y quien lo ejecuta.
Seguridad: lo del cliente es público#
Esta sección es la más importante del capítulo. Léela con calma.
Todo lo que Vite incluye en el bundle acaba en el navegador del usuario. Está en texto plano. Cualquiera puede verlo.
Cuando haces npm run build, Vite recorre tu código, sustituye cada import.meta.env.VITE_API_URL por su valor real, y mete todo en los ficheros de dist/. El resultado es JavaScript estático que sirves desde un servidor. No hay proceso que proteja ese código: el navegador lo descarga y lo ejecuta.
Compruébalo tú mismo: abre las herramientas de desarrollo de cualquier app web, ve a la pestaña “Fuentes” (Sources) o busca en los ficheros .js de dist/. Los valores que pusiste con prefijo VITE_ están ahí.
La consecuencia es directa:
// Esto esta MAL — una clave secreta con prefijo VITE_ va al bundle y es publica
// visible en DevTools
const CLAVE_STRIPE = import.meta.env.VITE_STRIPE_SECRET_KEY;
// Esto esta bien — una URL publica de API no es un secreto
// nadie gana nada sabiendola
const API_URL = import.meta.env.VITE_API_URL;La diferencia no está en .gitignore. Está en lo que el código hace con la variable. Si la usas en el frontend, está en el bundle. Si está en el bundle, es pública.
Los secretos reales viven en el backend.
El flujo correcto es: el cliente pide datos al backend de la app (sin credenciales). El backend usa sus propias variables de entorno para llamar a servicios externos con su clave privada. En Node.js, esas variables se leen con process.env (la forma equivalente a import.meta.env pero para el servidor, no para el navegador). Solo devuelve al cliente lo que el cliente necesita ver.
Navegador → pide datos al backend → Backend usa process.env.STRIPE_SECRET_KEY
para llamar a Stripe
← recibe los datos listos ← Backend devuelve solo el resultadoEl navegador nunca toca la clave. La clave nunca sale del servidor. El nivel 7 del curso profundiza en cómo montar ese backend y estructurar esta separación.
Node vs Vite (de pasada)#
Si has visto código de Node.js, habrás visto process.env.NOMBRE_VARIABLE. Eso es cómo Node lee las variables de entorno en el servidor.
En un proyecto Vite (frontend), la forma equivalente es import.meta.env.VITE_NOMBRE. No son intercambiables: process.env no existe en el navegador y import.meta.env no es la forma estándar de Node.
En los proyectos Node que no usan Vite, la librería dotenv hace exactamente lo que Vite hace aquí de forma automática: lee un fichero .env y carga sus valores en process.env. Con Vite no necesitas instalarla, pero la verás en proyectos de backend.
La regla práctica es simple: en el código que corre en el navegador (todo lo que está en src/), usa import.meta.env. En código de servidor (Node, backend), process.env. El nivel 9 del curso cubre Node.js en profundidad.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Por qué solo las variables que empiezan por VITE_ llegan al navegador?
Tu turno#
Abre el Team Builder que llevas del capítulo anterior y practica el ciclo completo: saca la URL de la API a .env.local, léela con import.meta.env, añade lógica por entorno y, si llegas al nivel excelente, deja explícita la frontera entre lo público y lo secreto.
Ejercicio · hazlo en local
Variables de entorno en el Team Builder
En tu Team Builder con Vite hay una URL de API hardcodeada en el código. Tu tarea es sacarla a un fichero .env.local, leerla con import.meta.env.VITE_API_URL, y si llegas al nivel excelente, dejar claro en el código y en los ficheros de configuración cuál es la frontera entre lo que puede ir en el cliente y lo que no.
Paso 1: Que no haya URLs en el código
- Creas un fichero `.env.local` en la raíz del proyecto con la variable `VITE_API_URL=<tu-url>`.
- En el código lees la variable con `import.meta.env.VITE_API_URL` en lugar de escribir la URL directamente.
- Creas un `.env.example` con la clave pero sin el valor real, para que sirva de plantilla.
- El proyecto sigue funcionando con `npm run dev` y la petición va a la misma URL que antes.
Paso 2: Que dev y prod sean distintos
- Añades lógica con `import.meta.env.MODE` o `import.meta.env.PROD` para mostrar información de depuración solo en desarrollo.
- El `.env.example` explica el propósito de cada variable con un comentario.
- Puedes cambiar la URL de la API cambiando solo `.env.local`, sin tocar el código.
Paso 3: Que la frontera de seguridad sea explícita
- El código incluye un comentario claro de por qué ciertos valores NO pueden ir con prefijo VITE_ (o llevan un ejemplo de lo que nunca debes hacer).
- El `.env.example` distingue entre config pública (VITE_) y secretos que van en el backend.
- Hay un `.gitignore` en el proyecto que incluye `.env*.local` para que los valores reales nunca se suban a git.
- Puedes explicar de memoria por qué un secreto con prefijo VITE_ es visible para cualquiera aunque no esté en git.
Cómo hacerlo en local
Clona el repositorio del curso, entra en la carpeta del ejercicio y abre el
index.html en tu navegador. Toda tu solución va en
solucion.js.
git clone <repo>
cd exercises/nivel-4/variables-de-entorno
# abre index.html en el navegador y edita solucion.js Ver soluciones
// Lee la URL base de la API desde la variable de entorno (no la hardcodees).
// definida en .env.local
const API_URL = import.meta.env.VITE_API_URL;
// misma app, distinta URL por entorno
fetch(API_URL + '/heroes')
.then(function (r) { return r.json(); })
.then(function (heroes) {
// el array de heroes llegados del servidor
console.log(heroes);
});
// .env.local va al .gitignore: los valores reales NO se versionan.
// Cualquier companero que clone el repo copia .env.example a .env.local
// y rellena sus propios valores; el codigo no cambia. Por qué este nivel
- La URL de la API deja de estar hardcodeada: el mismo código apunta a dev o prod según la variable. No hace falta tocar el código para desplegar en otro entorno.
- El .env.example documenta qué variable necesita el proyecto sin revelar ningún valor real.
// import.meta.env.MODE vale 'development' con npm run dev
// y 'production' cuando haces npm run build.
// booleano de conveniencia (true en build)
const enProduccion = import.meta.env.PROD;
// Lee la URL de la API (definida en .env.local, no en el codigo).
const API_URL = import.meta.env.VITE_API_URL;
// Solo en desarrollo mostramos informacion de depuracion.
// En produccion, la consola queda limpia para el usuario final.
if (!enProduccion) {
console.log('Modo dev: conectando con la API en ' + API_URL);
}
fetch(API_URL + '/heroes')
.then(function (r) { return r.json(); })
.then(function (heroes) {
// el array de heroes
console.log(heroes);
}); Por qué es mejor que el anterior
- Un .env.example versionado dice qué variables hacen falta sin filtrar valores; cualquier colaborador que clone el repo sabe exactamente qué tiene que crear.
- La lógica por MODE separa comportamientos dev/prod: la consola de depuración solo aparece en desarrollo, donde es útil, y no ensucia la experiencia del usuario final.
// CORRECTO: en el cliente solo va lo PUBLICO.
// Una URL base de API no es un secreto: el usuario la puede ver en las peticiones
// de red de cualquier modo (pestaña Red en DevTools). Tenerla en .env.local
// simplemente permite cambiarla por entorno sin tocar el codigo.
// definida en .env.local
const API_URL = import.meta.env.VITE_API_URL;
// true solo en el build de produccion
const enProduccion = import.meta.env.PROD;
if (!enProduccion) {
console.log('Dev: API en ' + API_URL);
}
fetch(API_URL + '/heroes')
.then(function (r) { return r.json(); })
.then(function (heroes) {
// el array de heroes del Team Builder
console.log(heroes);
});
// PROHIBIDO: una clave real con prefijo VITE_ acaba en el bundle = visible para
// cualquiera que abra DevTools -> Ver fuente o Network -> Preview.
// Un secreto en el frontend NO es secreto, por mucho que este en .env.local.
//
// const KEY = import.meta.env.VITE_STRIPE_SECRET_KEY; // <- NUNCA
// const DB = import.meta.env.VITE_DATABASE_URL; // <- NUNCA
//
// Los secretos reales viven en el BACKEND. El backend hace la llamada a la API
// privada y devuelve al cliente solo los datos que este necesita ver.
// Nivel 7 del curso cubre como montar ese backend y como el frontend se comunica
// con el sin exponer ninguna credencial. Por qué es mejor que el anterior
- Demuestras la frontera: en el cliente solo lo público; el secreto real se queda en el backend. Es la diferencia entre una app segura y una credencial filtrada.
- Los comentarios del código no son decorativos: son la regla escrita en el sitio donde alguien podría romperla. Un comentario de prohibición junto al patrón incorrecto evita que el próximo desarrollador cometa el error.
- El .gitignore del proyecto protege todos los ficheros .env*.local de forma explícita. No depende de que alguien se acuerde.