Du ødelægger dine miljøvariabler allerede i dev

Du ødelægger dine miljøvariabler allerede i dev

Byg kører fint, du klikker “Deploy”, og fem minutter senere stirrer du på en hvid skærm og en fejl i konsollen: Cannot read properties of undefined (reading 'API_URL'). Velkommen til miljøvariabler.

Jeg har mistet mere tid på forsvundne process.env værdier end på nogen anden type bug i små projekter. Det gode er, at når du først har en mental model, falder 80% af fejlene i samme få kategorier.

Miljøvariabler er bare konfiguration, ikke magi

Start med at aflaste hjernen: en miljøvariabel er bare tekst, der kommer udefra. Ikke en særlig datatype. Ikke en “hemmeligheds-boks”. Bare tekst.

I Node ligger de typisk på process.env. I Vite-projekter ender de som import.meta.env.VITE_... i din frontend. Fælles for dem er, at de ikke defineres i koden, men i det miljø koden kører i.

Det lyder simpelt, men for at bruge dem rigtigt skal du skille tre ting ad:

  • hvor værdien er defineret (OS, hosting panel, .env-fil)
  • hvornår værdien læses (under build eller når koden rent faktisk kører)
  • om koden kører i browseren eller på serveren

Hvis de tre ikke matcher det, du tror, får du klassikeren: “men det virker jo lokalt”. Jeg har skrevet om det generelle deploy-kaos i en anden artikel på Coding Class: Hvis det kun virker på din maskine, virker det ikke. Her holder vi fokus på miljøvariabler.

Miljøvariabel vs. secret vs. almindelig config

Der går tit rod i begreberne, så lad mig skære det ud.

Miljøvariabel: En nøgle-værdi, som miljøet stiller til rådighed. F.eks. PORT=3000 eller NODE_ENV=production. Teknik, ikke sikkerhed i sig selv.

Secret: En følsom værdi, som ikke må ende i frontend eller logs. F.eks. database password, API-nøgler til Stripe, JWT-secrets. De lever ofte som miljøvariabler, men det gør dem ikke automatisk sikre.

Almindelig config: Ting der godt må være offentlige: feature flags, API-base-url til din egen backend, app-navn osv. De kan også ligge som miljøvariabler, men det er mere for fleksibilitet end for sikkerhed.

Pointen: miljøvariabler er bare en måde at give konfiguration til din app. Sikkerhed handler om hvor værdierne ender, ikke hvordan de startede.

Byggetid vs køretid – den forskel der dræber dine Vite-projekter

Den hyppigste fejl jeg ser: man forventer at kunne ændre en env-var i hosting panelet og så ændrer frontend-adfærden sig magisk uden et nyt build. Det gør den ofte ikke.

Forestil dig to tidspunkter i dit projekt:

Byggetid (build time): Når du kører npm run build. Koden pakkes, bundtes, minificeres. For Vite betyder det, at alle import.meta.env.VITE_... læses og deres værdier bliver hardcodet ind i JavaScript-filerne.

Køretid (runtime): Når brugeren åbner siden, eller din Node-server processer et request. Her kan koden læse det, der ligger i miljøet lige nu.

Frontend-konfig: fastfrosset ved build

Hvis du har noget som:

// src/config.ts
export const API_URL = import.meta.env.VITE_API_URL

så sker der det her under build:

  • Vite læser VITE_API_URL fra dit miljø/.env-filer
  • indsætter værdien direkte i den bundtede fil, f.eks. "https://api.example.com"
  • den færdige .js-fil indeholder ikke længere nogen reference til import.meta.env

Derfor: ændrer du env-vars i Vercel eller Netlify efter build, ændrer du ikke den allerede deployede frontend. Du skal bygge igen.

Backend-konfig: læses ved startup

I en simpel Node-backend:

// server.ts
import http from "http"

const PORT = process.env.PORT || 3000

http.createServer((req, res) => {
  res.end("Hello")
}).listen(PORT)

her læses process.env.PORT, når processen starter. Hvis hosting-platformen ændrer miljøvariablen og genstarter processen (eller du selv gør), får du den nye værdi uden nyt build.

Så din mentale model kan være:

Frontend (Vite, React osv.): miljøvariabler er i praksis build-konstant. Backend (Node, Express osv.): miljøvariabler er runtime-konfiguration (læses ved opstart).

Det er derfor “undefined i production” på frontend ofte handler om manglende VITE_-prefix eller en env-var, der ikke var sat på det tidspunkt, hvor buildet kørte.

Frontend må aldrig kende dine secrets

Nu til den ubehagelige sandhed: alt, hvad du sender til browseren, er offentligt. Uanset om det kom fra en .env-fil, en fancy “secret manager” eller blev hardcodet i koden.

Det gælder også miljøvariabler via Vite. Der er én simpel regel:

Hvis værdien lander i bundlet JavaScript, er den ikke hemmelig.

Det kan godt være, du ikke viser den direkte i UI, men brugeren kan altid åbne devtools, søge i kildekoden eller kigge på network requests.

Hvad må så gerne i frontend-env?

Jeg plejer at bruge denne tommelfingerregel:

  • URL’er til din egen backend (f.eks. VITE_API_BASE_URL)
  • feature flags der styrer UI (f.eks. VITE_ENABLE_EXPERIMENTAL_UI)
  • offentlige nøgler, der er designet til at være offentlige (f.eks. Stripe public key)

Alt andet, der har noget med penge, adgang eller signering at gøre, bør bo i backend. Frontenden kalder din backend, og backenden snakker med 3. parts API’er med rigtige secrets.

Hvis du er i tvivl, så antag at det ikke må ligge i frontend. Din fremtidige jeg vil være taknemmelig.

.env-filer lokalt – det stille rod der bryder prod

Lokalt vil du næsten altid ende med en eller flere .env-filer. De er praktiske, men de er også en skjult kilde til “det virker hos mig”-bugs.

Et simpelt setup til et Vite + Node-projekt kunne være:

  • .env.local til ting, der kun gælder dig (lokale passwords, porte osv.)
  • .env eller .env.development til delt udviklingskonfiguration
  • .env.example som “skabelon” uden rigtige secrets

.env og venner skal næsten altid i din .gitignore. Det gælder især filer med rigtige secrets. Det du til gengæld gerne vil commite, er .env.example:

# .env.example
VITE_API_BASE_URL=
DATABASE_URL=
JWT_SECRET=

Så kan en ny udvikler på projektet bare:

cp .env.example .env.local
# og så udfylde værdierne

Typisk fejl jeg selv har lavet mere end én gang: jeg opdaterer en env-var lokalt, men glemmer at opdatere .env.example. Næste person på projektet får gamle keys, og vi fejlsøger et problem, der kun eksisterer på deres maskine.

Hvis du vil se et lidt større eksempel på config-struktur, har jeg en artikel om monorepos hvor env-vars også spiller en rolle: Byg et lille monorepo uden at drukne i tooling.

NODE_ENV – hvad betyder den egentlig?

NODE_ENV er en af de mest misforståede env-vars. Mange pakker kigger på den for at slå debug-funktioner til eller fra.

Typisk bruger man:

  • NODE_ENV=development når du kører lokalt med hot reload
  • NODE_ENV=production når du kører den rigtige deploy

I Node bruges den ofte til at styre logging, cache osv. I bundlere bruges den også til at fjerne dødkode:

if (process.env.NODE_ENV === "development") {
  console.log("Extra debug")
}

Ved build med NODE_ENV=production kan bundleren ofte se, at betingelsen aldrig er sand og fjerne hele blokken.

Pointen: du skal ikke overbelaste NODE_ENV. Brug den til “er vi i prod eller ej”, og lav dine egne env-vars til alt andet.

Vite og VITE_-prefixet der redder dig fra at lække secrets

Hvis du har prøvet at skrive import.meta.env.API_URL og få undefined, så har du allerede mærket Vites sikkerhedsdesign.

Vite eksponerer kun miljøvariabler til frontend, hvis de starter med VITE_. Resten er tilgængelige i build-processen, men lander ikke i browser-koden.

Sådan læser du env-vars i Vite

Et simpelt eksempel:

// .env
VITE_API_BASE_URL="http://localhost:3000"
SECRET_API_KEY="abc123" # kommer IKKE ud i frontend
// src/api.ts
const baseUrl = import.meta.env.VITE_API_BASE_URL

export async function fetchUsers() {
  const res = await fetch(`${baseUrl}/users`)
  return res.json()
}

Her får du kun VITE_API_BASE_URL i frontend. SECRET_API_KEY vil ikke være tilgængelig som import.meta.env.SECRET_API_KEY. Den er stadig til stede i build-processens miljø, men Vite lader den ikke slippe ud.

Typisk begynderfejl: du har sat API_URL i Vercel, men i koden skriver du import.meta.env.VITE_API_URL. Resultat: undefined. Løsning: giv variablen et VITE_-prefix både lokalt og i hosting-panelet.

Modes og .env-filer i Vite

Vite har “modes” som styrer hvilke env-filer der bruges. Groft sagt:

  • .env bruges altid
  • .env.development bruges når du kører vite / npm run dev
  • .env.production bruges når du kører vite build

Vercel og Netlify sætter typisk NODE_ENV=production under build, så dine .env.production-værdier er relevante, hvis du bruger dem. Mange vælger dog at lade hosting-panelet styre alt og kun bruge env-filer lokalt.

Du kan finde flere detaljer i Vites egen dokumentation, som faktisk er ret læsbar: Vite env og modes.

Node-backend: brug process.env tidligt og højt

I backend-land bliver tingene både simplere og mere farlige. Simpler, fordi du kun har én kontekst: Node. Farligere, fordi du typisk håndterer rigtige secrets.

Mit bedste råd: læs og valider alle env-vars ved startup. Ikke nede i tilfældige services.

// config.ts
type Config = {
  port: number
  databaseUrl: string
  jwtSecret: string
}

export function loadConfig(): Config {
  const { PORT, DATABASE_URL, JWT_SECRET } = process.env

  if (!DATABASE_URL) throw new Error("DATABASE_URL is required")
  if (!JWT_SECRET) throw new Error("JWT_SECRET is required")

  return {
    port: PORT ? Number(PORT) : 3000,
    databaseUrl: DATABASE_URL,
    jwtSecret: JWT_SECRET,
  }
}
// server.ts
import { loadConfig } from "./config"

const config = loadConfig()

console.log("Starting server on", config.port)

Fordelen er, at hvis du glemmer at sætte en env-var i produktion, fejler appen ved opstart i stedet for halvvejs inde i et request. Det er nemmere at opdage og logs er mere tydelige.

Der findes også biblioteker til det her (f.eks. zod kombineret med process.env), men mønstret er det samme: valider én gang, tidligt.

Vercel og Netlify – hvorfor er min env undefined i production?

Hvis du hoster på Vercel eller Netlify, er miljøvariabler en UI-ting i deres dashboard. Det gør det nemt at ændre, men det skjuler også, hvad der egentlig sker.

Vercel: build-env vs runtime-env

På Vercel har du tre miljøer: Development, Preview og Production. Hvert miljø kan have sine egne env-vars.

For et Vite-projekt gør Vercel i grove træk:

  • starter en build-job med de env-vars du har sat for det miljø
  • kører npm run build (eller hvad du har angivet)
  • hoster den genererede frontend som statiske filer

Hvis du ændrer en env-var i Vercel og ikke trigger et nyt build, ser din bundtede frontend de gamle værdier.

Typiske fejlkilder:

  • env-variablen er kun sat i “Development”, men dit deploy kører i “Production”
  • du har glemt VITE_-prefixet
  • du har sat env-var efter sidste build

Til Node backends på Vercel (Serverless Functions) læses process.env ved køretid for hver function execution, men værdierne kommer fra samme sted i dashboardet.

Dokumentationen er værd at skimpe: Vercel environment variables.

Netlify: samme idé, lidt anden UX

Netlify har også environment variables per site. De bruges både til build og til eventuelle serverless functions.

Flowet ligner Vercel meget:

  • du sætter env-vars i Site settings
  • de er tilgængelige når Netlify bygger din frontend
  • de er også tilgængelige som runtime-vars i functions

Hvis din Vite-app ser undefined, er opskriften den samme: tjek navn, prefix og at du faktisk har lavet et nyt deploy efter at have ændret værdierne.

Netlifys dokumentation om emnet: Netlify environment variables.

Fejlsøgning når alt bare hedder undefined

Et simpelt mønster du kan følge, når din env-var forsvinder på vej til prod:

  • Log udvalgte env-vars i build-loggen (uden secrets). F.eks. console.log('VITE_API_BASE_URL', import.meta.env.VITE_API_BASE_URL) i en lille build-only fil.
  • Tjek i dashboardet at variablen er sat i det rigtige miljø (Production vs Preview).
  • Søg i den deployede bundle (via devtools “Search in all files”) efter værdien for at se, om den overhovedet er kommet med.

Det føles lidt gammeldags at søge i bundlet JS, men det er ofte den hurtigste måde at bekræfte, om problemet er build eller runtime.

En lille env-tjekliste der redder dig fra de værste fejl

Hvis jeg skulle koge det ned til få vaner, ville det være de her.

1. Skeln frontend og backend skarpt

Beslut eksplicit hvad der er frontend-config, og hvad der er backend-config. Alt privat og følsomt ryger i backend. Frontenden får kun de værdier, den skal bruge, og alle med VITE_-prefix.

2. Valider backend-env ved startup

Lav en lille config.ts som tjekker process.env og fejler højt og tydeligt, hvis noget mangler. Ingen stille undefined, der først eksploderer senere.

3. Brug .env.example som kontrakt

Hold .env.example opdateret. Hver gang du tilføjer en ny env-var, tilføjer du den der. Tænk på filen som dokumentation for, hvad appen forventer af sit miljø.

4. Husk build vs runtime i dine deploys

Når du ændrer env-vars på Vercel/Netlify, skal du tænke: skal der et nyt build til? Hvis det handler om frontend, er svaret næsten altid ja. Hvis det “kun” er backend (functions), kan en genstart være nok.

5. Log klogt i prod uden at lække noget

Du behøver ikke logge hele process.env i produktion (gør det ikke), men du kan logge f.eks. hvilken base-url der er valgt, eller om et feature flag er slået til. Hellere en bekræftelse i loggen end gætteri.

Hvis du vil bygge videre på de her vaner, er 12-factor app-princippet om config et fint, overskueligt sted at kigge: 12factor.net/config. Det er ikke raketvidenskab, bare lidt disciplin.

Jeg er ret sikker på, miljøvariabler bliver ved med at overraske nye udviklere de næste mange år. Spørgsmålet er mest, hvor hurtigt vi lærer at bruge dem som et bevidst værktøj i stedet for en magisk fejl-generator.

Lever konfigurationen i en fil der læses ved runtime, fx /config.json, eller injicer værdier i et globalt objekt som window.__ENV fra din server eller deploy-script. Frontend henter så disse værdier med fetch eller læser window.__ENV ved opstart, og du kan opdatere uden ny bundling.
Sæt aldrig følsomme nøgler i frontend-miljøvariabler; kald i stedet dine tredjeparts-APIer fra en server-side endpoint eller en proxy som har secret'en. Brug en secret manager (fx AWS Secrets Manager, HashiCorp Vault eller hosting-providernes secrets) og begræns nøglernes rettigheder og levetid.
Valider og fail-fast ved app-start: brug en schema-validator (zod, joi eller dotenv-safe) til at tjekke påkrævede variabler og giv en klar fejlmelding. Tilføj også et .env.example og CI-tjek der sikrer at de nødvendige keys findes før deploy.
Hold et .env.example i repoet med kun nøgle-navne, gitignore faktiske .env-filer, og brug navnekonventioner eller tools som dotenv-flow eller Vites .env.[mode] for per-environment filer. Håndter production-secrets i hostingpanelet eller en secret manager, ikke i kildekoden.

Lasse Falkenberg er typen, der begyndte at rode med HTML og CSS for at lave en simpel bandside – og opdagede, at det var langt sjovere at få knapperne til at virke end at stå på scenen. Siden har han kastet sig over alt fra små JavaScript-snippets til Python-scripts, der kan spare ham for kedeligt, manuelt arbejde i hverdagen.

Han har lært det meste ved at bygge ting, der lige præcis løser hans egne problemer: en lille webapp til at holde styr på brætspilsaftener, et script til at rydde op i rodede mapper, eller en enkel side til at dele noter med venner. Undervejs har han kæmpet sig gennem alle de klassiske fejl – semikolon, forkerte indrykninger og variabler, der hedder noget helt andet end man tror – og det er præcis den rejse, han deler på Coding Class.

På Coding Class skriver Lasse praktiske, jordnære guides, der tager udgangspunkt i små, konkrete opgaver: noget du kan se, teste og bygge videre på med det samme. Han elsker at bryde en opgave ned i små bidder, vise den fulde kode og forklare linje for linje, hvad der sker – inklusive de typiske bugs, du med stor sandsynlighed også støder på.

For Lasse handler kodning ikke om flotte titler eller store ord, men om følelsen af at få noget til at virke – og om at du som læser kan gå derfra med noget, du selv har bygget. Hvis du kan kende glæden ved at få en fejl til endelig at forsvinde, er du lige på bølgelængde med hans måde at lære fra sig på.

Send kommentar

You May Have Missed