Environment variables are essential for modern web development. They allow you to configure your app for different environments (development, staging, production) without changing the source code.
But while they're straightforward to use in backend code, they can be misused or misunderstood in frontend applications.
๐ What are environment variables?
Environment variables are key-value pairs injected into your app's runtime environment. They're used to store configuration, not code.
Examples:
NODE_ENV=production
DATABASE_URL=postgres://user:pass@host/db
API_URL=https://api.myapp.com
These values are not hardcoded in your source files, they're passed in from .env
files, deployment platforms, or shell scripts.
๐ฅ๏ธ Backend: node.js, express, etc.
โ How to use them
In Node.js apps, environment variables are commonly loaded using dotenv:
# .env
PORT=4000
SECRET_KEY=mysecret
DATABASE_URL=postgres://user:pass@localhost/mydb
// server.js
require('dotenv').config();
const express = require('express');
const app = express();
app.listen(process.env.PORT, () => {
console.log(`Server running on port ${process.env.PORT}`);
});
๐ Best practices
- โ Store secrets (tokens, DB credentials) in env vars
- โ
Use
.env.local
for development-specific config - โ
Use
process.env.VARIABLE
directly, not hardcoded values - โ Validate required variables before starting your app
// Validate environment variables at startup
const requiredEnvVars = ['PORT', 'SECRET_KEY', 'DATABASE_URL'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
Tip: Fail fast, don't let your app start if essential config is missing.
๐งโ๐ป Frontend: react, vite, next.js, etc.
โ ๏ธ Important difference
Frontend code runs in the browser, so not all env vars are safe to expose.
When you build your frontend app, build tools like Vite, Create React App, or Next.js only include environment variables with specific prefixes in the client-side bundle.
Framework | Public Prefix Required | Example |
Vite | VITE_ | VITE_API_URL |
Create React App | REACT_APP_ | REACT_APP_API_URL |
Next.js | NEXT_PUBLIC_ | NEXT_PUBLIC_API_URL |
# .env
VITE_API_URL=https://api.example.com
VITE_ANALYTICS_ID=GA-123456789
// inside a React/Vite component
const apiUrl = import.meta.env.VITE_API_URL;
fetch(`${apiUrl}/users`);
๐ How frontend build process works
During the build process, your build tool replaces environment variables with their actual values:
// Your source code:
fetch(import.meta.env.VITE_API_URL + '/users')
// Final bundled code:
fetch('https://api.example.com/users')
This means anyone can see these values by inspecting your JavaScript bundle.
โ ๏ธ Never expose secrets
Any env var used in the frontend is visible to anyone who opens DevTools or views your source code.
โ Safe for frontend:
VITE_API_URL
REACT_APP_ANALYTICS_ID
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
โ Never in frontend:
VITE_SECRET_KEY
REACT_APP_DB_PASSWORD
NEXT_PUBLIC_STRIPE_SECRET_KEY
๐ฑ Next.js special considerations
Next.js has additional complexity due to its hybrid nature:
// Server-side only (API routes, getServerSideProps)
const secretKey = process.env.SECRET_KEY; // No prefix needed
// Client-side (components, pages)
const publicUrl = process.env.NEXT_PUBLIC_API_URL; // Prefix required
Runtime vs Build-time: Next.js can access server-side env vars at runtime, but client-side vars are embedded at build time.
๐ก๏ธ Protecting secrets
โ Keep secrets server-side only
For example, in a full-stack app:
- Use
VITE_API_URL
in the frontend to talk to your own backend - Keep the
SECRET_KEY
, DB credentials, and third-party tokens in the backend.env
If your frontend needs to interact with a third-party API requiring authentication, proxy it through your backend:
// Backend API route
app.get('/api/external-data', async (req, res) => {
const response = await fetch('https://api.third-party.com/data', {
headers: {
'Authorization': `Bearer ${process.env.SECRET_API_KEY}`
}
});
const data = await response.json();
res.json(data);
});
// Frontend code
fetch('/api/external-data') // No secrets exposed
.then(response => response.json())
.then(data => console.log(data));
๐ฆ Managing env files
Recommended file structure:
.env # Default values for all environments
.env.development # Development-specific overrides
.env.staging # Staging-specific overrides
.env.production # Production-specific overrides
.env.local # Machine-specific config (never committed)
๐ File precedence order
Environment files are loaded in this order (highest priority first):
.env.local
(highest priority).env.development
/.env.staging
/.env.production
.env
(lowest priority)
๐งผ Git ignore
Always add sensitive files to your .gitignore
:
# .gitignore
.env.local
.env*.local
.env.production
Note: You might want to commit .env.development
and .env
with safe default values, but never commit files with real secrets.
๐ง Common mistakes
Mistake | Why It's Bad | How to Fix |
Exposing secrets in frontend | They're readable by anyone | Keep secrets in backend only |
Hardcoding URLs in code | Can't switch environments easily | Use env vars with proper prefixes |
Using wrong prefix (e.g. API_URL ) | Won't be available in frontend bundle | Use VITE_ , NEXT_PUBLIC_ , etc |
Committing .env with secrets | Leaks credentials in version control | Use .gitignore properly |
Not validating required variables | App crashes at runtime | Validate at startup |
Mixing build-time and runtime vars | Unexpected behavior in production | Understand when vars are resolved |
๐ Debugging environment variables
Check what's available:
// Backend
console.log('All env vars:', process.env);
// Frontend (Vite)
console.log('Available env vars:', import.meta.env);
// Frontend (Create React App)
console.log('Available env vars:', process.env);
Common debugging steps:
- Check file naming:
.env
notenv.txt
- Verify prefixes:
VITE_API_URL
notAPI_URL
- Restart dev server: Changes require restart
- Check file location:
.env
should be in project root - No quotes needed:
API_URL=https://example.com
notAPI_URL="https://example.com"
โ Summary
- Use environment variables to keep config out of your code
- Backend: Use
process.env
directly, validate required vars, never hardcode secrets - Frontend: Only expose non-sensitive values with the correct prefix (
VITE_
,REACT_APP_
,NEXT_PUBLIC_
) - Security rule: If it's used in the frontend, it's public to everyone
- File management: Use
.env.local
for secrets,.gitignore
sensitive files - Production: Configure env vars through your deployment platform
๐ Final thoughts
Environment variables are a simple but powerful tool. When used correctly, they keep your apps secure, portable, and configurable, whether you're deploying to Vercel, Docker, or bare metal.
Just remember the golden rule:
If it's used in the frontend, it's public. If it's sensitive, keep it on the server.
With proper understanding of how environment variables work in both frontend and backend contexts, you can build applications that are both secure and maintainable across different environments.