Vite Env Variable Undefined? It Needs the VITE_ Prefix
You set API_URL=http://localhost:3005 in a Vite project's .env, but import.meta.env.API_URL logs undefined in the browser, and every API request goes to the wrong address.
Encountered this while building an AI Agent SaaS platform for a client — recording the root cause and the fix.
TL;DR
Vite exposes only VITE_-prefixed environment variables to client code — a security design that keeps server secrets out of the browser bundle.
# ❌ never exposed to the frontend
API_URL=http://localhost:3005
# ✅ exposed to the frontend
VITE_API_URL=http://localhost:3005
Access it as import.meta.env.VITE_API_URL. TypeScript projects should add one more step: an env.d.ts declaration for proper autocomplete.
Symptoms
Two typical presentations:
Symptom 1: the variable is undefined
// .env: API_URL=http://localhost:3005
console.log(import.meta.env.API_URL) // undefined
console.log(import.meta.env) // BASE_URL, MODE, PROD... are all there — just not yours
The .env file clearly loaded (MODE, PROD and the other built-ins are present), so this is not a loading failure — the exposure rule filtered your variable out.
Symptom 2: prefix added, access name mistyped
// .env: VITE_API_URL=...
const url = import.meta.env.VITE_APIURL // undefined — case must match exactly
A separate note: "environment variable reads undefined" on the Node.js server side has a different common cause (dotenv load order) — see Node.js env loaded undefined: dotenv import order. Don't mix the two investigations.
Root Cause: The Prefix Rule Is a Security Boundary
Vite's design problem: .env files typically hold two kinds of values — public config the frontend needs (an API base URL) and values that must never reach a browser (database URLs, third-party API keys). Exposing everything means one oversight puts a secret into a publicly served static bundle.
So Vite draws a hard line: only VITE_-prefixed variables appear in import.meta.env. Everything else is visible only in vite.config.ts (the Node side) via loadEnv.
The exposure mechanism matters too: static replacement at build time. Vite inlines import.meta.env.VITE_API_URL as a string literal during the build; at runtime there is no "read the environment" step. Two consequences:
- The value appears in plain text inside the shipped JS — a
VITE_variable is public by nature - Changing environment at runtime (container env, system variables) never affects an already-built bundle — switching environments requires a rebuild, or a runtime-injected config (e.g.
window.__CONFIG__)
The Fix: Prefix + Type Declaration
Step 1: add the VITE_ prefix in .env. Keep the name semantic; the prefix is only an exposure marker:
# .env
VITE_API_URL=http://localhost:3005
Step 2: read it through one config module, not scattered import.meta.env calls in components:
// src/config.ts
export const API_URL = import.meta.env.VITE_API_URL
Step 3 (TypeScript projects): declare the types in src/env.d.ts for full autocomplete:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Separate dev and production values with mode files — Vite picks the right one automatically:
.env.development # dev server
.env.production # npm run build
.env # loaded in both — shared config goes here
After editing any .env file, restart the dev server — env files load once at startup, and hot reload does not refresh them. This is the second most common "I changed it but nothing happened" cause.
Watch out
A VITE_ variable is public information: its value is inlined into the client bundle in plain text. API base URLs and feature flags are fine; database connection strings and third-party secrets are not — those stay in server-side environment variables. When the frontend needs protected config, serve it from an API endpoint instead of .env.
FAQ
Where do Vite environment variables live?
In the .env family at the project root: .env loads in both modes, .env.development only for the dev server, .env.production only during builds. After editing any of them you must restart the dev server — Vite loads env files once at startup, and hot reload does not refresh them.
Can I put secrets behind the VITE_ prefix?
No. VITE_ variables are statically inlined into the client bundle at build time — anyone can read them in the page source. The prefix exists to separate publishable config (API base URLs, feature flags) from server-only secrets. Secrets belong in server-side environment variables only.
What is the difference between process.env and import.meta.env in Vite?
import.meta.env is the browser-side API — Vite statically replaces VITE_-prefixed keys into it. process.env is a Node.js API, available only inside vite.config.ts and SSR contexts. Writing process.env.XXX in frontend code is always undefined after build.
CCLEE
Independent developer, 24 years in e-commerce, focused on grounding AI in real business scenarios.
Work with me