Environment variables in Node.js and Vite
Hard-coded URLs and keys make an app fragile and unsafe. How .env files work in Node.js and Vite, which values reach the browser, and how to validate them at startup.
An application runs in several places: your laptop, a staging server, production. The code is the same in each. The database address, the API URL and the keys are not. Environment variables keep those differences out of the code, so the same build can run anywhere.
In Node.js
Put local values in a .env file at the project root.
DATABASE_URL=postgres://localhost:5432/shop
PORT=3000
Current Node.js versions can load the file without a library:
node --env-file=.env server.js
On older versions, the dotenv package does the same job with import "dotenv/config" at the top of the entry file. Either way, the values appear on process.env:
const port = Number(process.env.PORT ?? 3000);
In Vite
Vite reads .env files by itself and picks the right one for the current mode.
# .env.development
VITE_API_URL=http://localhost:3000/api
# .env.production
VITE_API_URL=https://api.example.com
const res = await fetch(`${import.meta.env.VITE_API_URL}/products`);
npm run dev uses the development file and npm run build uses the production one. The files are loaded in this order, with later ones winning:
| File | Loaded | Commit it? |
|---|---|---|
.env |
Always | Yes, if it holds no secrets |
.env.local |
Always | No |
.env.[mode] |
Only in that mode | Yes, if it holds no secrets |
.env.[mode].local |
Only in that mode | No |
The rule that prevents leaks
Vite exposes only variables that start with VITE_, and it writes their values into the JavaScript bundle. Next.js does the same with NEXT_PUBLIC_.
So a prefixed variable is public. Anyone can read it in the browser. An API URL is fine there. A secret key is not. Secrets belong in server code only, as described in keeping API keys out of your frontend.
Validate at startup
A missing variable should stop the app immediately with a clear message. It should not cause a strange error three hours later.
const required = ["DATABASE_URL", "SESSION_SECRET"];
const missing = required.filter((name) => !process.env[name]);
if (missing.length > 0) {
throw new Error(`Missing environment variables: ${missing.join(", ")}`);
}
For anything larger, describe the configuration with a schema library and parse process.env once. You get types and validation together.
Keep them out of Git
.env
.env.local
.env.*.local
Add those lines to .gitignore, and commit a .env.example that lists every variable with a placeholder. New developers then know what to fill in.
In production
Do not copy .env files to servers or bake them into images. Set the values in your hosting platform's settings or a secret manager. They are injected when the app starts, can be changed without a new build, and stay out of the repository.
A short checklist
- No URL, key or password is hard-coded.
- Only values that may be public carry a public prefix.
- The app refuses to start when a required variable is missing.
.envfiles with real values are ignored by Git..env.exampleis up to date.