Why Is My Environment Variable Undefined? (Vite, Next.js, Node)
Your .env file has the value but the code sees undefined. The reasons are almost always the same: the wrong prefix (VITE_, NEXT_PUBLIC_), the wrong file name or folder, not restarting the dev server, build-time vs runtime, or the variable never being set in production.
console.log(process.env.API_URL) // undefined
console.log(import.meta.env.API_URL) // undefined
The value is right there in your .env file, so why can't the code see it? It's one of the most common beginner problems, and there are only a handful of causes. Work through them in order.
(New to this? Environment variables explained covers the basics.)
1. You didn't restart the dev server
.env files are read when the server starts. Change the file → stop the dev server (Ctrl+C) → start it again. Hot reload doesn't pick up .env changes in most setups.
2. Frontend code needs a special prefix
Browser code can't see your environment variables unless you explicitly expose them, because anything sent to the browser is public. Each framework uses a prefix as the "yes, I mean to make this public" switch:
| Framework | Prefix | Read with |
|---|---|---|
| Vite (React, Vue, Svelte) | VITE_ |
import.meta.env.VITE_API_URL |
| Next.js (client components) | NEXT_PUBLIC_ |
process.env.NEXT_PUBLIC_API_URL |
| Create React App (legacy) | REACT_APP_ |
process.env.REACT_APP_API_URL |
| Astro | PUBLIC_ |
import.meta.env.PUBLIC_API_URL |
So API_URL=... won't work in browser code; VITE_API_URL=... will.
Important: the prefix makes the value public. Never put a secret key behind VITE_ or NEXT_PUBLIC_ — anyone can read it in the browser. Secrets belong in server code only. (Keep API keys out of an AI-built app)
3. Wrong syntax for the tool
- Vite uses
import.meta.env, notprocess.env. - Plain Node.js does not read
.envfiles on its own. Either load it:
node --env-file=.env server.js
(built into modern Node), or use the dotenv package at the very top of your entry file.
- In Next.js, read the variable directly as
process.env.NEXT_PUBLIC_X. Dynamic access likeprocess.env[name]isn't replaced at build time in client code.
4. Wrong file name or location
- The file must be in the project root (next to
package.json), not insrc/. - Exact name:
.env,.env.local,.env.development. Notenv,.env.txt(Windows can hide that.txt), or.ENV. - In a monorepo, the
.envmust be in the app's own folder, or loaded explicitly.
5. Formatting in the .env file
API_URL=https://api.example.com ✅
API_URL = https://api.example.com ⚠️ spaces can break some loaders
export API_URL=... ⚠️ works in some loaders, not others
API_URL="value with spaces" ✅ quote values with spaces or #
6. It works locally but is undefined in production
Your .env file isn't deployed (it's in .gitignore, as it should be — what is .gitignore?). You must set the variables in your host's dashboard or server config too.
And for frontend variables there's an extra twist: they're baked in at build time. Vite and Next.js replace import.meta.env.VITE_X with the literal value when you run the build. So:
- The variable must be set when the build runs, not just when the app starts.
- After changing it, you must rebuild and redeploy — restarting isn't enough.
Server-side variables (read in API routes or server code) are read at runtime, so a restart is enough for those.
(Why your app works locally but not in production covers more of these gaps.)
Quick checklist
- Restarted the dev server?
- Using the right prefix for browser code?
import.meta.envfor Vite,process.envfor Node/Next?- File named exactly
.env, in the project root? - Set in production, and was it set at build time?
The summary
- Restart after editing
.env. - Browser code needs
VITE_/NEXT_PUBLIC_— and those values are public. - Node needs
--env-fileordotenvto read.envat all. - Frontend variables are fixed at build time: set them before building, rebuild after changing.
EasySpawn keeps your environment variables on the server, available to builds and to the running app alike, so development and production read the same settings. See how it works or join the waitlist.
Related: Environment Variables Explained · How to Keep API Keys Out of an AI-Built App · Secrets Management Beyond .env Files · What Is Vite?
Keep reading
502 Bad Gateway: What It Means and How to Fix It
A 502 means the server in front of your app (Nginx, Cloudflare, a load balancer) couldn't get a valid answer from your app. The usual causes — app crashed, wrong port, still starting, out of memory — and a step-by-step way to find which one.
500 Internal Server Error: How to Find What Actually Broke
A 500 error means your server-side code threw an error it didn't handle. The page tells you nothing on purpose; the real message is in your logs. Where to look, the most common causes in AI-built apps, and how to stop users seeing a bare 500.