Variables de Entorno
Receta
Configura y usa de forma segura variables de entorno en Next.js 15+ con archivos .env, el prefijo NEXT_PUBLIC_ para exposición del cliente, configuración en tiempo de ejecución, y el patrón server-only para prevenir fuga de secretos.
Ejemplo Funcional
Estructura del Archivo .env
# .env.local (ignorado por git, anulaciones locales)
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
AUTH_SECRET="super-secret-key-never-expose"
# Las variables seguras para el cliente deben usar el prefijo NEXT_PUBLIC_
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_abc123"# .env (registrado en git, valores predeterminados compartidos)
NEXT_PUBLIC_APP_NAME="My App"# .env.production (anulaciones de producción)
NEXT_PUBLIC_APP_URL="https://myapp.com"Acceso a Entorno con Seguridad de Tipo
// lib/env.ts
import "server-only";
function requireEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}
export const env = {
DATABASE_URL: requireEnv("DATABASE_URL"),
AUTH_SECRET: requireEnv("AUTH_SECRET"),
} as const;Acceso a Entorno Seguro para el Cliente
// lib/env-client.ts
export const clientEnv = {
appUrl: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
appName: process.env.NEXT_PUBLIC_APP_NAME ?? "My App",
stripeKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "",
} as const;Uso de Zod para Validación
// lib/env.ts
import "server-only";
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
NODE_ENV: z.enum(["development", "production", "test"]),
});
export const env = envSchema.parse(process.env);
// lib/env-client.ts
const clientEnvSchema = z.object({
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_APP_NAME: z.string(),
});
export const clientEnv = clientEnvSchema.parse({
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
});Uso de Server Component
// app/dashboard/page.tsx
import { env } from "@/lib/env";
export default async function DashboardPage() {
// Seguro: esto se ejecuta solo en el servidor
const data = await fetch(`${env.DATABASE_URL}/api/data`);
return <div>{/* renderizar datos */}</div>;
}Inmersión Profunda
Cómo Funciona
- Next.js carga archivos
.envautomáticamente en este orden de prioridad (el más alto gana):.env.$(NODE_ENV).local>.env.local>.env.$(NODE_ENV)>.env. - Solo las variables con prefijo
NEXT_PUBLIC_se integran en línea en el bundle del cliente en tiempo de compilación. Todas las otras referencias aprocess.envse reemplazan conundefineden el código del cliente. process.envno es un objeto real en el código del cliente. Next.js realiza reemplazo de cadena estática en tiempo de compilación. El acceso dinámico comoprocess.env[key]no funcionará en componentes del cliente.- El paquete
server-onlycausa un error en tiempo de compilación si un módulo se importa desde un Componente del Cliente, proporcionando una garantía firme de que los secretos permanecen en el servidor. - Las variables de entorno se integran en tiempo de compilación para páginas estáticas. Para configuración en tiempo de ejecución, usa Route Handlers o
serverRuntimeConfig/publicRuntimeConfigennext.config.js(patrón heredado de Pages Router).
Variaciones
Variables de Entorno en Tiempo de Ejecución (Docker):
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Expone env del lado del servidor en tiempo de ejecución (no en tiempo de compilación)
serverRuntimeConfig: {
databaseUrl: process.env.DATABASE_URL,
},
// Expone al servidor y cliente en tiempo de ejecución
publicRuntimeConfig: {
apiUrl: process.env.NEXT_PUBLIC_API_URL,
},
};
export default nextConfig;Usar T3 Env para Validación Completa:
// env.mjs
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
AUTH_SECRET: process.env.AUTH_SECRET,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
},
});Notas de TypeScript
- Crea un
env.d.tspara aumentarProcessEnvpara autocompletado:
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
AUTH_SECRET: string;
NEXT_PUBLIC_APP_URL: string;
NEXT_PUBLIC_APP_NAME: string;
}
}- Los objetos env validados con Zod proporcionan inferencia de tipo completa sin anotaciones de tipo adicionales.
Trampa
- El acceso a propiedades dinámicas no funciona en el código del cliente.
process.env[varName]siempre seráundefineden el cliente porque Next.js realiza reemplazo de cadena estática, no búsqueda en tiempo de ejecución. .env.localno se carga en entornos detestpor defecto. Usa un archivo.env.test.localo carga env manualmente en tu configuración de pruebas.- Cambiar archivos
.envrequiere reiniciar el servidor de desarrollo. La recarga en caliente no detecta cambios en variables de entorno. - Los valores
NEXT_PUBLIC_son visibles en el bundle del navegador. Nunca pongas secretos (claves de API, URLs de base de datos, secretos de autenticación) detrás de este prefijo. - Las compilaciones de Docker integran variables de entorno en tiempo de compilación. Para variables de entorno en tiempo de ejecución en contenedores, usa el modo de salida
standaloney establece variables de entorno en el contenedor en ejecución, no en el pasoRUNdel Dockerfile.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
Archivos .env con NEXT_PUBLIC_ | Integrado, cero configuración | Sin validación, sin env en tiempo de ejecución |
Validación Zod en lib/env.ts | Seguridad de tipo, falla rápida | Configuración manual |
T3 Env (@t3-oss/env-nextjs) | Validación completa, división cliente/servidor | Dependencia adicional |
next.config.js runtimeConfig | Env verdadero en tiempo de ejecución | Patrón heredado, no nativo de App Router |
| Platform env (Vercel, AWS) | Seguro, por entorno | Configuración específica del proveedor |
Preguntas Frecuentes
¿Por qué process.env[varName] retorna undefined en el código del cliente?
- Next.js realiza reemplazo de cadena estática en tiempo de compilación, no búsqueda en tiempo de ejecución.
- Solo las referencias literales como
process.env.NEXT_PUBLIC_APP_URLse reemplazan. - El acceso a propiedades dinámicas (por ejemplo,
process.env[key]) no se puede resolver en tiempo de compilación y retornaundefined.
¿Cuál es el orden de prioridad de carga para archivos .env?
.env.$(NODE_ENV).local(prioridad más alta).env.local.env.$(NODE_ENV).env(prioridad más baja)- Los archivos de mayor prioridad anulan los de menor prioridad.
.env.localestá ignorado por git por defecto.
¿Qué sucede si pones un secreto (como una URL de base de datos) detrás del prefijo NEXT_PUBLIC_?
- El valor se integra en línea en el bundle JavaScript del lado del cliente en tiempo de compilación.
- Cualquiera que inspeccione el código fuente del navegador o el bundle puede ver el secreto.
- Nunca pongas claves de API, URLs de base de datos o secretos de autenticación detrás de
NEXT_PUBLIC_.
¿Por qué cambiar un archivo .env requiere reiniciar el servidor?
- Las variables de entorno se cargan una vez cuando el servidor de desarrollo se inicia.
- La sustitución de módulos en caliente (HMR) no vuelve a leer archivos
.env. - Debes detener y reiniciar
next devpara que los cambios surtan efecto.
¿Cómo protege import "server-only" los secretos en lib/env.ts?
- El paquete
server-onlycausa un error en tiempo de compilación si un Componente del Cliente importa el módulo. - Esto proporciona una garantía firme de que las variables de entorno solo del servidor nunca aparecen en el bundle del navegador.
¿Cómo se manejan las variables de entorno en tiempo de ejecución en contenedores de Docker?
- Las variables
NEXT_PUBLIC_siempre se integran en tiempo de compilación y no pueden cambiar en tiempo de ejecución. - Las variables del lado del servidor deben establecerse en el contenedor en ejecución (
docker run -e KEY=value). - Usa el modo
output: "standalone"para queprocess.envlea suceda en tiempo de ejecución para código de servidor.
Trampa: ¿Por qué .env.local no se carga durante entornos de test?
- Next.js omite
.env.localcuandoNODE_ENV=testpor defecto. - Usa
.env.test.localen su lugar, o carga manualmente archivos de entorno en tu configuración de pruebas.
¿Cómo se escriben variables de entorno en TypeScript para autocompletado?
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
AUTH_SECRET: string;
NEXT_PUBLIC_APP_URL: string;
}
}- Esto aumenta la interfaz global
ProcessEnvpara queprocess.env.DATABASE_URLreciba autocompletado.
¿Qué ventaja tiene la validación con Zod sobre una función requireEnv simple?
- Zod valida el formato (por ejemplo,
.url(),.min(32)) no solo la presencia. - Proporciona inferencia de tipo completa, por lo que el objeto retornado está completamente escrito automáticamente.
- La validación se ejecuta al inicio, fallando rápido con mensajes de error claros antes de que se sirva cualquier solicitud.
Trampa: ¿Qué sucede si usas variables NEXT_PUBLIC_ con output: "standalone" en Docker?
- Los valores
NEXT_PUBLIC_se reemplazan estáticamente en tiempo denext build. - Establecerlos como variables de entorno en tiempo de ejecución de Docker no tiene efecto en el código del cliente.
- Debes reconstruir la imagen si algún valor
NEXT_PUBLIC_cambia.
¿Qué es T3 Env y cómo difiere de una configuración manual con Zod?
- T3 Env (
@t3-oss/env-nextjs) proporciona una única llamadacreateEnvque separa esquemas de servidor y cliente. - Valida automáticamente que las variables
NEXT_PUBLIC_estén en la secciónclienty las variables de servidor estén enserver. - Una configuración manual con Zod requiere que manejes la división cliente/servidor por ti mismo.
¿Cómo se escribiría el tipo del valor de retorno de un objeto env validado con Zod en TypeScript?
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
});
// El tipo se infiere automáticamente:
// { DATABASE_URL: string; AUTH_SECRET: string }
export const env = envSchema.parse(process.env);- No se necesita anotación de tipo manual; Zod infiere el tipo del esquema.
Relacionado
- Autenticación - almacenamiento seguro de
AUTH_SECRET - Despliegue - variables de entorno en Docker y producción
- Route Handlers de API - acceso a variables de entorno en Route Handlers
- Documentación de Variables de Entorno de Next.js