Full-project lesson · needs Node.js, npm and real project files, so it does not run in the in-page playground.
A real project runs in several environments: your computer, a test or “staging” site, and production. Each needs different settings. This lesson shows how to organise them safely with Vite. It builds on Environment Variables.
Which file wins?
Vite loads .env first and then lets more specific files override it. When you run npm run build, the mode is production, so .env.production overrides .env.
Create a shared .env with a development value:
VITE_API_URL=http://localhost:3000
and a .env.production with the real value:
VITE_API_URL=https://api.example.com
The app prints which one it received:
function App() {
return <p>API: {import.meta.env.VITE_API_URL}</p>
}
export default App
We built this project with vite build and checked two things: the page shows https://api.example.com, and the string http://localhost:3000 does not appear anywhere in the built files. The development value never reaches production, which is exactly what you want.
Modes
A mode is a name that selects which .env.[mode] files load. The defaults are development for vite and production for vite build. You can create more:
npx vite build --mode staging # loads .env and .env.staging
Typical setup:
| File | Used for |
|---|---|
.env | Defaults shared by all modes |
.env.development | npm run dev |
.env.staging | A test site that mirrors production |
.env.production | npm run build |
.env.local, .env.*.local | Personal overrides; ignored by Git |
Keep the variable names the same in every file and change only the values. Document them in .env.example.
Build-time vs runtime configuration
Vite inlines VITE_ values at build time. That has a consequence: one build equals one environment. To deploy the same app to staging and production, you build twice, each with its own values.
If you want a single build that you can promote from staging to production, load configuration when the app starts, from a file served next to it:
{ "apiUrl": "https://api.example.com" }
// Load it once, before rendering the app
const config = await fetch('/config.json').then((r) => r.json())
createRoot(document.getElementById('root')).render(<App config={config} />)
The deployment process then replaces config.json per environment. Because it is a public file, it can hold only public settings, never secrets. Make sure config.json is not cached forever (give it a short cache time).
What belongs in configuration
| Good | Bad |
|---|---|
| The API base URL | A private API key or token |
| A public analytics or site id | A database password |
A feature flag (VITE_SHOW_BETA=true) | An admin credential |
| A maps key restricted to your domain | A payment secret key |
| The app’s public URL | Anything you would not publish on your site |
Everything that reaches the browser is public; see Why Secret Keys Must Not Be Exposed.
Feature flags
A simple flag lets you ship code that is not enabled yet:
const showBeta = import.meta.env.VITE_SHOW_BETA === 'true' // env values are strings
{showBeta && <BetaBanner />}
Remember that import.meta.env.VITE_X is always a string: compare with 'true', not true. For per-user flags, use a server or a feature-flag service.
Validate configuration early
A missing variable should fail clearly, not as a mysterious “undefined” bug later:
export function requireEnv(name) {
const value = import.meta.env[name]
if (!value) throw new Error(`Missing environment variable ${name}`)
return value
}
export const API_URL = requireEnv('VITE_API_URL')
Different base paths and URLs
Also configure the public path and the URLs your app uses per environment: the base option, the router basename, the API URL and the canonical URL for SEO. A staging site often lives at a different domain or in a subfolder (Deployment Fundamentals).
Common mistakes
- Putting production secrets into
.envand committing them. - Expecting to change
VITE_values after the build. - Different variable names per environment, so the code needs conditionals.
- Forgetting to restart the dev server after editing
.env. - Reading booleans and numbers from env without converting them from strings.
Key points
.env.productionoverrides.envinnpm run build; the development value never reaches the production bundle.VITE_values are inlined at build time, so one build serves one environment. Use a runtimeconfig.jsonfor build-once, deploy-many.- Configuration is public: no secrets, early validation, and consistent names.