Environment Variables: Best Practices for Developers

Environment variables are one of the most important concepts in modern software development, yet they're often misused. From storing API keys to configuring different environments, here's everything you need to know to use them correctly.

What Are Environment Variables?

Hardcoding an API key into source code is one of those mistakes that seems minor until it isn't — until the repo goes public, or a contractor gets access, or a screenshot ends up in a Slack channel. Environment variables exist precisely to keep secrets out of code, and the pattern is simple enough that there's really no excuse not to use it.

The concept comes from the Unix operating system, where programs could access a set of variables through the process environment. Every modern operating system and deployment platform supports them, from your local laptop to AWS Lambda, Kubernetes, and Vercel.

Why Use Environment Variables?

The core reason to use environment variables is the separation of code and configuration. The Twelve-Factor App methodology — a widely-adopted guide for building software-as-a-service apps — states that configuration should be stored in the environment, not in the code.

  • Security: API keys and passwords never appear in your source code or version history
  • Flexibility: The same codebase can connect to different databases in development, staging, and production
  • Portability: Anyone can clone and run your project by setting their own environment variables
  • Auditability: You can change configuration without modifying or deploying code

The .env File Pattern

The most common pattern for local development is the .env file. This is a plain text file in your project root that contains key-value pairs. Libraries like dotenv (Node.js) or python-dotenv load these values into the process environment when your application starts.

# .env — never commit this file!
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
STRIPE_SECRET_KEY=sk_test_abc123...
JWT_SECRET=my-super-secret-key-change-in-production
PORT=3000
NODE_ENV=development
ENABLE_FEATURE_X=true
⚠️ Never Commit .env to Git

Add .env to your .gitignore immediately. Thousands of developers have accidentally leaked API keys, database passwords, and private keys by committing .env files. Once pushed to a public repository, credentials are compromised — even if you delete the file later, the history remains. Always add .env to .gitignore before your first commit.

The .env.example Pattern

Commit a .env.example file with all the required variable names but no real values. New team members copy this file, fill in their own values, and are immediately productive. This solves the documentation problem without ever committing secrets.

# .env.example — safe to commit, shows required variables
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
STRIPE_SECRET_KEY=sk_test_your_key_here
JWT_SECRET=generate-a-random-string-here
PORT=3000
NODE_ENV=development

Reading Environment Variables in Code

Node.js

// Install dotenv: npm install dotenv
require('dotenv').config();  // Load .env file

const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;

if (!dbUrl) {
  throw new Error('DATABASE_URL environment variable is required');
}

Python

# Install python-dotenv: pip install python-dotenv from dotenv import load_dotenv import os load_dotenv() # Load .env file db_url = os.getenv('DATABASE_URL') port = int(os.getenv('PORT', '3000')) if not db_url: raise ValueError('DATABASE_URL is required')

Docker and Docker Compose

# docker-compose.yml — reference .env file
services:
  app:
    build: .
    env_file:
      - .env
    environment:
      - NODE_ENV=production
      - PORT=3000

Naming Conventions

Environment variables traditionally use SCREAMING_SNAKE_CASE (all uppercase with underscores). This convention distinguishes them visually from regular variables in code. Some established conventions:

  • DATABASE_URL: Full connection string to the database
  • PORT: The port the server should listen on
  • NODE_ENV / APP_ENV: The current environment (development, staging, production)
  • *_SECRET_KEY or *_API_KEY: Sensitive credentials with clear naming
  • NEXT_PUBLIC_* or VITE_*: Framework prefixes that expose variables to the frontend

Frontend vs Backend Variables

Be very careful about which environment variables are exposed to the browser. Any variable accessible in client-side JavaScript is visible to every user who visits your site — even if you think it's hidden.

Frontend frameworks use naming conventions to distinguish safe public values from secrets. In Next.js, only variables prefixed with NEXT_PUBLIC_ are bundled into the client. In Vite, only VITE_ prefixed variables are exposed. All other variables stay server-side only.

Never expose secret API keys to the browser

If your Stripe secret key, OpenAI API key, or database password appears in your frontend JavaScript bundle, anyone can find it and use your services at your expense. Always call external APIs from your backend server, not directly from the browser.

Production Secrets Management

For production environments, .env files on servers are often insufficient. Dedicated secrets management tools provide better security, rotation, and audit trails:

  • AWS Secrets Manager: Store and automatically rotate secrets, integrated with IAM
  • HashiCorp Vault: Open-source secrets management for complex multi-cloud setups
  • Doppler: Developer-friendly secrets manager that syncs across environments
  • Vercel / Netlify Environment Variables: Built-in UI for managing variables in deployment platforms
  • GitHub Secrets: Encrypted secrets for GitHub Actions workflows

Validation at Startup

A crucial best practice is validating all required environment variables when your application starts, before accepting any requests. Failing fast with a clear error is much better than discovering a missing variable when a user tries to make a payment at 2 AM.

// Validate required env vars at startup
const required = [
  'DATABASE_URL',
  'JWT_SECRET',
  'STRIPE_SECRET_KEY'
];

const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
  console.error(`Missing required env vars: ${missing.join(', ')}`);
  process.exit(1);
}

Conclusion

Environment variables are a foundational tool for building secure, portable, and maintainable applications. The key rules: never hardcode secrets in your code, always add .env to .gitignore, commit .env.example to document what's needed, and validate required variables at startup. As your application scales, consider moving to a proper secrets management system rather than relying solely on .env files.