Skip to content

Fix: Vite Environment Variables Not Working

FixDevs ·

Quick Answer

How to fix Vite environment variables showing as undefined — missing VITE_ prefix, wrong .env file for the mode, import.meta.env vs process.env, TypeScript types, and SSR differences.

The Error

An environment variable is undefined in your Vite application:

console.log(import.meta.env.VITE_API_URL);  // undefined
console.log(import.meta.env.MY_SECRET);      // undefined
console.log(process.env.VITE_API_URL);       // undefined or TypeError

Or TypeScript reports an error:

Property 'VITE_API_URL' does not exist on type 'ImportMetaEnv'

Or the variable is available in development but undefined after vite build.

Why This Happens

Vite’s environment variable system has specific rules that differ from Node.js, webpack, and Create React App:

  • Missing VITE_ prefix — only variables prefixed with VITE_ are exposed to client-side code. Variables without this prefix are intentionally hidden for security. MY_SECRET is undefined; VITE_MY_SECRET works.
  • Using process.env instead of import.meta.env — Vite doesn’t polyfill process.env. Use import.meta.env in client code.
  • Wrong .env file for the mode — Vite loads different .env files depending on the --mode flag. Running vite (development) loads .env.development, running vite build (production) loads .env.production. A variable in .env.development won’t be available in production builds.
  • .env file not in the project root — Vite looks for .env files in the project root by default (where vite.config.js is). Files in subdirectories are ignored.
  • TypeScript type definitions missingimport.meta.env.VITE_* works at runtime but TypeScript shows an error because ImportMetaEnv isn’t extended with your variable names.
  • SSR mode differences — in server-side rendering, import.meta.env behaves differently and some variables may need different handling.

Fix 1: Add the VITE_ Prefix

This is the most common cause. Rename your variables in the .env file:

# .env — WRONG (not exposed to client)
API_URL=https://api.example.com
SECRET_KEY=abc123

# .env — CORRECT (VITE_ prefix exposes to client)
VITE_API_URL=https://api.example.com
# Leave truly secret variables without VITE_ — they stay server-only
SECRET_KEY=abc123  # Still usable in vite.config.js but NOT in browser code
// In your component/page
const apiUrl = import.meta.env.VITE_API_URL;
console.log(apiUrl);  // "https://api.example.com" ✓

Why the prefix? Vite statically replaces import.meta.env.VITE_* at build time. Without the prefix, secrets like database passwords and API keys in your .env could be accidentally bundled and exposed in the client JavaScript. The prefix is an explicit opt-in for client exposure.

Fix 2: Use import.meta.env, Not process.env

Vite uses import.meta.env for environment variables in client code, not process.env:

// WRONG — process.env is not available in Vite client code
const apiUrl = process.env.VITE_API_URL;  // undefined

// CORRECT
const apiUrl = import.meta.env.VITE_API_URL;

Vite does replace process.env.NODE_ENV as a special case, but this is the only process.env variable you can rely on. For everything else, use import.meta.env.

Migration from Create React App:

CRAVite
REACT_APP_API_URLVITE_API_URL
process.env.REACT_APP_API_URLimport.meta.env.VITE_API_URL
process.env.NODE_ENVimport.meta.env.MODE
process.env.PUBLIC_URLimport.meta.env.BASE_URL

Fix 3: Use the Right .env File for Each Mode

Vite loads .env files based on the current mode:

FileWhen loaded
.envAlways
.env.localAlways, overrides .env (not committed to git)
.env.developmentvite (dev server)
.env.development.localvite dev server, local override
.env.productionvite build
.env.production.localvite build, local override
.env.stagingvite --mode staging

Common mistake: Putting variables in .env.development then wondering why they’re undefined after vite build. Add variables to both files or put shared values in .env:

# .env — shared across all modes
VITE_APP_NAME=MyApp

# .env.development — dev only
VITE_API_URL=http://localhost:8080

# .env.production — production only
VITE_API_URL=https://api.example.com

Use a custom mode:

# Build with staging mode
vite build --mode staging

# Create .env.staging
VITE_API_URL=https://staging.api.example.com
// Check the current mode
console.log(import.meta.env.MODE);  // "development", "production", or "staging"

Fix 4: Check the .env File Location

Vite looks for .env files in the root directory — where vite.config.js or vite.config.ts lives. If your project structure nests these differently:

project/
├── vite.config.ts    ← Vite root
├── .env              ← Correct location
├── src/
│   └── .env          ← Wrong — ignored by Vite
└── frontend/
    └── .env          ← Wrong if vite.config.ts is in project/

Override the env directory in vite.config.ts:

import { defineConfig } from 'vite';

export default defineConfig({
  envDir: './config/env',  // Load .env files from this directory instead
});

Verify which file Vite is reading by adding a unique test variable and logging it:

# .env
VITE_DEBUG_CHECK=loaded_from_root

# Then in your app
console.log(import.meta.env.VITE_DEBUG_CHECK);  // "loaded_from_root" if correct

Fix 5: Add TypeScript Type Definitions

If TypeScript shows Property 'VITE_API_URL' does not exist on type 'ImportMetaEnv', add type declarations:

// src/vite-env.d.ts (create this file)
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_NAME: string;
  readonly VITE_FEATURE_FLAG: string;
  // Add all your VITE_ variables here
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

This file also enables autocomplete in editors. After adding it, restart the TypeScript language server.

Make variables optional if they might not be defined:

interface ImportMetaEnv {
  readonly VITE_API_URL: string;           // Required
  readonly VITE_FEATURE_FLAG?: string;     // Optional
}

Validate at runtime:

// src/config.ts
const apiUrl = import.meta.env.VITE_API_URL;
if (!apiUrl) {
  throw new Error('VITE_API_URL is not defined. Check your .env file.');
}

export const config = {
  apiUrl,
  appName: import.meta.env.VITE_APP_NAME ?? 'MyApp',
};

Fix 6: Use define for Non-VITE_ Variables

For variables you want injected at build time without the VITE_ prefix, use the define option in vite.config.ts:

import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ mode }) => {
  // Load all env variables (including non-VITE_ ones)
  const env = loadEnv(mode, process.cwd(), '');

  return {
    define: {
      // Explicitly expose specific non-VITE_ variables
      'process.env.BUILD_DATE': JSON.stringify(new Date().toISOString()),
      '__APP_VERSION__': JSON.stringify(process.env.npm_package_version),
      // Only expose what you explicitly choose — don't spread all of env
    },
  };
});
// In your app code
console.log(__APP_VERSION__);       // "1.2.3"
console.log(process.env.BUILD_DATE); // "2026-03-19T..."

Warning: Never expose database credentials, private API keys, or secrets via define. The values are inlined into the bundle and visible to anyone who downloads your JavaScript.

Fix 7: Fix Environment Variables in SSR

In Vite SSR (server-side rendering), environment variables work differently. Variables available via import.meta.env on the server are all env vars (not just VITE_ prefixed), but this does not carry over to client hydration:

// server.ts (SSR)
// All process.env variables are available here via import.meta.env
const dbUrl = import.meta.env.DATABASE_URL;  // Works in SSR

// But DATABASE_URL is still not in the client bundle

For frameworks like SvelteKit or Nuxt with Vite:

// SvelteKit — use $env/static/private for server-only
import { DATABASE_URL } from '$env/static/private';  // Server only

// $env/static/public for client-safe
import { PUBLIC_API_URL } from '$env/static/public';  // Client + server

Still Not Working?

Restart the dev server after changing .env files. Vite does not hot-reload environment variable changes. Stop the server (Ctrl+C) and run vite again.

Check for .env syntax errors. Values don’t need quotes for simple strings, but special characters can cause issues:

# Fine
VITE_API_URL=https://api.example.com

# Needs quotes if value contains spaces or special chars
VITE_APP_DESCRIPTION="My App with spaces"
VITE_REGEX="^[a-z]+$"

Check .gitignore. If .env.local is in .gitignore (it should be), make sure you actually created the file locally:

ls -la .env*
# Should show .env and .env.local (if you created it)

Verify the variable appears in the bundle. In vite build output, check dist/assets/*.js for your variable value. If it’s been statically replaced, you’ll see the actual string, not import.meta.env.VITE_API_URL. If you see undefined, the variable wasn’t set during build.

For related Vite issues, see Fix: Vite Failed to Resolve Import and Fix: Next.js Environment Variables Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles