Skip to main content

Don’t hardcode URLs

In the previous lesson, the frontend had http://localhost:8000 hardcoded. This breaks the moment you deploy — your production API isn’t at localhost. Environment variables let you configure these values per environment.
// ❌ Hardcoded — only works locally
fetch("http://localhost:8000/api/users")

// ✅ Environment variable — works everywhere
fetch(`${import.meta.env.VITE_API_BASE_URL}/api/users`)

.env files

An .env file stores key-value pairs that your application reads at startup:
# frontend/.env
VITE_API_BASE_URL=http://localhost:8000
# backend/.env
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
SECRET_KEY=your-secret-key-here
Each line is KEY=VALUE. No spaces around the =. No quotes needed (unless the value contains spaces).
Never commit .env files to Git. They contain secrets like database passwords and API keys. Add .env to your .gitignore file. Always.

Vite environment variables (frontend)

Vite has one important rule: frontend environment variables must start with VITE_.
# frontend/.env
VITE_API_BASE_URL=http://localhost:8000
VITE_APP_NAME=My App

# ❌ This WON'T be available in your React code
SECRET_KEY=abc123
Access them in your React code with import.meta.env:
// ✅ Available — starts with VITE_
console.log(import.meta.env.VITE_API_BASE_URL);
// "http://localhost:8000"

// ❌ Undefined — doesn't start with VITE_
console.log(import.meta.env.SECRET_KEY);
// undefined
The VITE_ prefix is a security feature. Your React code gets bundled and sent to the user’s browser — anyone can read it. The prefix prevents you from accidentally exposing secrets like DATABASE_URL or SECRET_KEY in your frontend bundle. Only variables you explicitly mark with VITE_ are included.

Using environment variables in React

// frontend/src/api/users.js
const API_URL = import.meta.env.VITE_API_BASE_URL;

export async function getUsers() {
  const response = await fetch(`${API_URL}/api/users`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

export async function createUser(userData) {
  const response = await fetch(`${API_URL}/api/users`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
Define the URL once, use it everywhere. When you deploy, just change the environment variable — no code changes needed.

Python environment variables (backend)

In Python, you already know this pattern. Use python-dotenv or Pydantic’s BaseSettings:
# backend/.env
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
SECRET_KEY=your-secret-key-here
ALLOWED_ORIGINS=http://localhost:5173
# backend/config.py
import os
from dotenv import load_dotenv

load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")
SECRET_KEY = os.getenv("SECRET_KEY")
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",")

.env.example — document what’s needed

Create a .env.example file that shows what variables are required, without the actual values:
# frontend/.env.example
VITE_API_BASE_URL=http://localhost:8000
# backend/.env.example
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
SECRET_KEY=change-me-to-a-random-string
ALLOWED_ORIGINS=http://localhost:5173
Commit .env.example to Git. When a new developer clones the project, they copy it to .env and fill in their values:
cp .env.example .env
This is a universal pattern across all frameworks and languages. Every professional project has .env.example committed and .env in .gitignore. It documents what configuration a project needs without exposing secrets.

Multiple environments

You can have different .env files for different situations:
# frontend/.env                 # Default (development)
VITE_API_BASE_URL=http://localhost:8000

# frontend/.env.production      # Used when building for production
VITE_API_BASE_URL=https://api.myapp.com
Vite automatically loads the right file:
  • npm run dev → reads .env
  • npm run build → reads .env.production (if it exists)
The companion repo uses VITE_API_BASE_URL (same concept, more explicit name). Its mobile app uses Expo’s equivalent prefix: EXPO_PUBLIC_API_BASE_URL.

What’s next?

Your URLs are configurable. But when your React frontend at localhost:5173 tries to call your FastAPI backend at localhost:8000, the browser blocks the request. This is CORS — and it’s the most common error when connecting frontend to backend.

Understanding CORS

Fix cross-origin errors when your React frontend talks to your FastAPI backend