Gemmer du også dine API-nøgler det forkerte sted?
Du skal holde op med at hardcode hemmeligheder i din kode
Hvis du kun tager én ting med herfra: alt hvad der er hemmeligt, skal bo uden for din kode, og kun læses ind via environment variables.
Jeg starter lige med et lille flashback: Jeg push’ede engang en privat API key til GitHub kl. 01.37 om natten. Kl. 01.39 havde en bot fundet den. Kl. 01.45 var der brugt for over 100 dollars på API-kald. Det var min frivillige introduktion til environment variables.
Hvad er en environment variable i virkeligheden?
Glem alt om buzzwords. En environment variable (env var) er bare en lille tekstværdi, som bliver givet til dit program udefra, når det kører.
Eksempel fra shell:
# sæt en env var midlertidigt og kør et program
API_KEY=abc123 node app.js
Inde i app.js kan du så læse den sådan her i Node:
console.log(process.env.API_KEY) // "abc123"
Pointen: Din kode inde i app.js kender ikke værdien på forhånd. Den siger bare: "giv mig det, der ligger i environment".
Det er det samme, der sker på en server. Du deployer den samme kode, men i produktion sætter du f.eks. NODE_ENV=production og dine rigtige nøgler. På din lokale maskine sætter du test-værdier.
.env-filer: snydeark til environment vars
At skrive API_KEY=... foran hver kommando bliver hurtigt trættende. Her kommer .env-filer ind.
En .env-fil er bare en tekstfil ved siden af din kode med linjer som:
API_KEY=abc123
DATABASE_URL=postgres://user:pass@localhost:5432/dbnavn
Biblioteket dotenv kan læse den fil og putte værdierne ind i process.env, så din kode kan bruge dem.
En typisk Node-startfil ser sådan her ud:
// index.js
require('dotenv').config() // læs .env ind i process.env
console.log(process.env.API_KEY)
Nu kan du bare køre:
node index.js
og API_KEY bliver hentet fra .env-filen.
Hvorfor din .env aldrig må committes
Her er den klassiske fejl: man laver en fin .env, alt virker, man laver git add ., commit, push, og så ligger alle hemmelighederne pænt i et offentligt repo.
Løsningen er simpel:
# .gitignore
.env
.env.local
.env.*.local
.env-filer skal i .gitignore. De er lokale for din maskine eller din server. Ikke et fælles dokument som README.
Hvis du vil dele eksempler med andre, så lav en .env.example:
# .env.example
API_KEY=<din api nøgle her>
DATABASE_URL=<din database url her>
Den kan du godt committe. Den indeholder formatet, ikke de rigtige værdier.
Frontend vs backend: hvad må være hemmeligt hvor?
Her begynder det at gøre ondt på mange begyndere. For på backend kan du holde ting hemmelige. På frontend kan du ikke.
Regel jeg selv bruger:
- Alt der kører på serveren: kan have hemmelige environment vars.
- Alt der kører i browseren: er offentligt, uanset hvad du kalder det.
Så selv hvis du putter din API key i en env var i dit frontend-build, vil den ende i den byggede JavaScript-fil, som alle kan hente.
Hvorfor en API key i frontend ikke er en hemmelighed
Forestil dig, at du bygger en lille app i Vite eller Create React App og laver:
const apiKey = import.meta.env.VITE_API_KEY
Ved build-tid erstatter Vite det med selve strengen:
const apiKey = "abc123"
Den ender i din bundle. Enhver kan åbne DevTools, gå i Network, hente din bundlende JS-fil og søge efter "abc123".
Det er lidt ligesom at hænge din cykelnøgle på styret og håbe, at "ingen ved jo hvilken nøgle, der passer til hvilken cykel". Jo, det finder de ud af.
Konsekvensen: Hvis dit frontend-kode kan bruge en nøgle, så kan angriberen også. Det er samme kode, samme nøgle.
Så hvad må ligge i frontend-envs?
Jeg plejer at skelne sådan her:
- Må gerne i frontend: ting der alligevel er offentlige eller ufarlige
- Må kun i backend: alt der kan misbruges til at lave handlinger på dine eller brugeres vegne
Eksempler på ting der er ok i frontend-env:
- Feature-flags (skal vi vise den nye beta-knap?)
- Offentlige base-URLs (f.eks. URL til dit offentlige API)
- Tracking IDs til analytics (ikke hemmelige keys, bare IDs)
Eksempler på ting der hører til i backend-env:
- Database-logins
- Private API keys (Stripe secret key, OpenAI secret key osv.)
- SMTP-login til mails
- GitHub tokens, deploy-nøgler osv.
Hvis du er i tvivl, så lad som om du er din egen onde tvilling: "Hvis jeg får den her værdi, hvad kan jeg så misbruge?" Hvis svaret er "en del", så hører den til på backend.
Eksempel: lille Node/Express server med dotenv
Lad os tage et konkret eksempel på sikker brug af env vars i backend.
Vi siger, at du vil lave et endpoint, der kalder et eksternt API med en hemmelig nøgle. Opsætning:
// server.js
require('dotenv').config()
const express = require('express')
const fetch = require('node-fetch')
const app = express()
const PORT = process.env.PORT || 3000
app.get('/weather', async (req, res) => {
try {
const apiKey = process.env.WEATHER_API_KEY
const city = req.query.city || 'Copenhagen'
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}`
)
const data = await response.json()
res.json({ temp: data.current.temp_c })
} catch (err) {
console.error(err)
res.status(500).json({ error: 'something went wrong' })
}
})
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`)
})
.env:
PORT=3000
WEATHER_API_KEY=hemlig_nøgle_her
Din frontend kalder kun /weather?city=... på din egen server. Den ser aldrig WEATHER_API_KEY.
Det her mønster er også det du vil bruge, hvis du f.eks. bygger simple projekter på Coding Class og senere vil skifte fra demo-keys til rigtige nøgler.
Vite og VITE_-prefix: hvad ender i bundlen?
Hvis du bruger Vite (eller moderne bundlere), er der lidt ekstra regler.
I Vite gælder:
- Env vars i frontend skal starte med
VITE_for at være tilgængelige i koden - Alt du læser via
import.meta.envbliver bundlet ind i dit build
Eksempel på en .env til Vite:
VITE_API_BASE_URL=https://api.minapp.dk
VITE_FEATURE_NEW_HEADER=true
SECRET_ADMIN_KEY=meget_hemmelig
Din Vite-frontend kan gøre:
const baseUrl = import.meta.env.VITE_API_BASE_URL
const showNewHeader = import.meta.env.VITE_FEATURE_NEW_HEADER === 'true'
Men: SECRET_ADMIN_KEY er ikke tilgængelig, fordi den ikke har VITE_-prefix. Vite ignorerer den helt i frontend.
Det gør det faktisk lidt sværere at dumme sig. Men husk: alt med VITE_ skal du regne med er offentligt.
Lille buildtime-eksperiment
Hvis du vil se forskellen på buildtime og runtime, kan du prøve:
- Lav en ny Vite-app
- Lav en
.envmedVITE_MESSAGE=hej_fra_env - Brug den i koden:
console.log('env message:', import.meta.env.VITE_MESSAGE)
- Kør
npm run build - Åbn den bundlende JS-fil i
dist/assets
Du vil kunne søge efter "hej_fra_env" i filen. Værdien er simpelthen skrevet direkte ind.
Hvis du så ændrer din .env efter build, vil din hostede side stadig vise den gamle værdi, indtil du bygger igen. Det er derfor environment vars i frontend er buildtime, ikke runtime, medmindre din platform understøtter noget særligt.
Vite-delen er godt dokumenteret i deres egen env-dokumentation, men jeg synes det giver bedre mening, når man har set det ske i en rigtig bundle.
Hvor sætter man env vars ved deploy?
.env-filer er rare lokalt. I produktion vil du typisk sætte env vars via din hostingplatforms UI eller konfiguration.
Netlify
- Gå til dit site i Netlify
- Build & deploy > Environment > Environment variables
- Tilføj f.eks.
VITE_API_BASE_URL
Netlify bruger dem til build. For rene frontend-projekter er det altså stadig buildtime-vars.
Vercel
- Project settings > Environment Variables
- Du kan sætte dem separat for Development, Preview og Production
I frameworks som Next.js kan nogle env vars også læses ved runtime på server-funktioner, men igen: alt hvad der bruges i client components er offentligt.
GitHub Actions
- Inde på repoet: Settings > Secrets and variables
- Actions > New repository secret
I din workflow-fil kan du så bruge:
env:
NODE_ENV: production
API_KEY: ${{ secrets.API_KEY }}
Godt trick hvis du f.eks. vil deploye en backend, der skal have hemmelige nøgler ved build eller deploy.
Fejlfinding: undefined env, forkert prefix og cache
Der er tre env-fejl jeg ser igen og igen, både hos mig selv og andre.
1. process.env.VAR_NAME er undefined
Typiske årsager:
- Du har glemt at loade
dotenvi Node (require('dotenv').config()) .env-filen ligger ikke i den mappe, du tror- Variablen er ikke sat i din deploy-platform
Tip: log alt indholdet af process.env lokalt (men aldrig i produktion logs). Bare for at se, hvad der faktisk er sat.
2. Env var i Vite er altid tom
Det er næsten altid prefixet. I Vite gælder:
MY_API_KEYvirker ikke i frontendVITE_MY_API_KEYvirker
Og du skal læse den via import.meta.env.VITE_MY_API_KEY, ikke process.env.MY_API_KEY.
3. Du ændrer .env, men intet sker i produktion
For frontend-projekter skyldes det næsten altid, at du glemmer, at env vars er buildtime.
Løsning:
- Opdater environment vars i din hostingplatform
- Trigg et nyt build (ofte via et nyt git-commit)
- Ryd cache, hvis din platform cache’er builds
På backend kan du som regel bare genstarte processen, så den læser environment igen.
Huskeregel: sikker håndtering af secrets
Jeg lovede en beslutningsregel. Her er min version, som minder meget om 12-factor app-tankegangen fra Config-sektionen.
- Læg aldrig secrets i koden (hverken i JS, Python, HTML eller config-filer)
- Læg aldrig secrets i git (heller ikke midlertidigt, git-historik glemmer ikke)
- Alt hemmeligt bor i environment vars på backend eller i CI/CD
- Alt med VITE_ eller andre frontend-prefix er offentligt og må ikke være "hemmeligt hemmeligt"
- Lav en .env.example, så andre ved hvilke vars der skal sættes uden at få dine rigtige nøgler
Hvis du vil lege lidt videre, kan du prøve at bygge et lille projekt hvor frontend kun taler med din egen backend, og backend står for kontakten til eksterne APIs. Det er et godt næste skridt, når man går fra små demoer til noget, der ligner rigtige apps.
Jeg er ret nysgerrig på, hvornår du første gang opdager, at environment vars faktisk gør dit liv lettere og ikke bare er ekstra opsætning, du skal igennem – måske bliver det dagen hvor du slipper for endnu en "works on my machine"-fejl?








Send kommentar
Du skal være logget ind for at skrive en kommentar.