A React application needs different configuration values depending on where it runs: the API URL in development points to http://localhost:8000 (or uses the Vite proxy at /api), in staging it points to a staging server, and in production to the production API. Hardcoding these values forces code changes between deployments. Vite’s .env file system provides a clean solution: each environment has its own file, values are injected at build time via import.meta.env, and sensitive values never reach the browser.
Environment Files
# .env.development — loaded by: npm run dev
VITE_API_BASE_URL=http://localhost:8000/api
VITE_APP_TITLE=Blog (Dev)
VITE_ENABLE_DEVTOOLS=true
# .env.staging — loaded by: vite build --mode staging
VITE_API_BASE_URL=https://api-staging.blog.example.com/api
VITE_APP_TITLE=Blog (Staging)
VITE_ENABLE_DEVTOOLS=false
# .env.production — loaded by: vite build (default production build)
VITE_API_BASE_URL=/api
VITE_APP_TITLE=Blog
VITE_ENABLE_DEVTOOLS=false
# .env.local — loaded in all modes, NOT committed to git
# Put secrets and local overrides here
VITE_SENTRY_DSN=https://your-sentry-dsn
# .gitignore should include:
# .env.local
# .env.*.local
VITE_ are exposed to the browser. Plain environment variables (without the VITE_ prefix) are available in Vite’s Node.js build process but not in the compiled JavaScript that runs in the browser. This is a security feature — it prevents accidentally leaking server-side secrets (database passwords, private API keys) into the client bundle. Any variable you want in the browser code must start with VITE_.src/config.js that reads all env variables in one place and exports them with sensible fallbacks: export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api". Every other file imports from @/config rather than reading import.meta.env directly. This makes it trivial to find all configuration usage and provides a single place to add validation (throw if a required variable is missing).import.meta.env with the VITE_ prefix is literally embedded in the compiled JavaScript bundle — anyone can view the source and read these values. Never put secrets (private API keys, payment processor secret keys, database URLs) in VITE_ variables. Only put values that are intentionally public: API base URLs, feature flags, app names, public keys (Stripe publishable key, Sentry DSN for the frontend).Config Module
// src/config.js — centralised configuration
const required = (key) => {
const value = import.meta.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
};
export const config = {
// API
apiBaseUrl: import.meta.env.VITE_API_BASE_URL ?? "/api",
// App
appTitle: import.meta.env.VITE_APP_TITLE ?? "Blog",
isDev: import.meta.env.DEV, // Vite built-in: true in dev
isProd: import.meta.env.PROD, // Vite built-in: true in prod
mode: import.meta.env.MODE, // "development" | "production" | "staging"
// Features
enableDevtools: import.meta.env.VITE_ENABLE_DEVTOOLS === "true",
// Optional services (only throw if actually needed at runtime)
sentryDsn: import.meta.env.VITE_SENTRY_DSN ?? null,
};
// Usage:
// import { config } from "@/config";
// const api = axios.create({ baseURL: config.apiBaseUrl });
Updating the RTK Query API Service
// src/store/apiSlice.js — use config instead of hardcoded URL
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { config } from "@/config";
export const blogApi = createApi({
reducerPath: "blogApi",
baseQuery: fetchBaseQuery({
baseUrl: config.apiBaseUrl, // ← reads from .env
prepareHeaders: (headers, { getState }) => {
const token = getState().auth?.accessToken
?? localStorage.getItem("access_token");
if (token) headers.set("Authorization", `Bearer ${token}`);
return headers;
},
}),
// ... endpoints
});
Production Build with Different Mode
# Standard production build (uses .env.production)
npm run build
# Staging build (uses .env.staging)
vite build --mode staging
# Preview production build locally
npm run preview
# Inspect what env vars are in the bundle (never put secrets here!)
vite build && grep -r "VITE_" dist/
Common Mistakes
Mistake 1 — Forgetting VITE_ prefix (variable is undefined)
❌ Wrong — variable not exposed to browser:
API_URL=http://localhost:8000 # no VITE_ prefix
import.meta.env.API_URL // undefined!
✅ Correct — prefix all browser-accessible variables:
VITE_API_BASE_URL=http://localhost:8000 # ✓
Mistake 2 — Committing .env.local to git
❌ Wrong — real API keys or passwords in version control:
✅ Correct — add .env.local and .env.*.local to .gitignore. Commit only .env.development and .env.production with non-secret defaults.
Quick Reference
| File | When Loaded | Committed to Git |
|---|---|---|
.env |
All modes | Yes (non-secret defaults) |
.env.development |
npm run dev |
Yes |
.env.production |
vite build |
Yes |
.env.local |
All modes (overrides) | No (add to .gitignore) |
.env.development.local |
Dev only (overrides) | No |