DEV Community

Binary Journal
Binary Journal

Posted on

Environment Variables the Safe Way

Why Environment Variables Matter

Every app has secrets: API keys, database URLs, admin passwords. Hardcoding them in source code is a one-way ticket to leaks. Even if your repo is private, you never know who forks it or what CI logs expose.

Environment variables are the standard way to keep configuration out of code. But using them safely requires a few habits that go beyond just process.env.

The Basics: Loading and Accessing

In Node.js, you read env vars with process.env. But you should not access them raw everywhere. Create a central config module that validates and exposes them.

// config.js
const required = ['DB_URL', 'API_KEY', 'PORT'];

for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing required env var: ${key}`);
  }
}

module.exports = {
  dbUrl: process.env.DB_URL,
  apiKey: process.env.API_KEY,
  port: parseInt(process.env.PORT, 10),
};
Enter fullscreen mode Exit fullscreen mode

Fail fast at startup. If a required variable is missing, crash immediately rather than failing later in a confusing way.

Never Commit .env Files

Tools like dotenv load variables from a .env file for local development. That file must stay out of version control.

Add .env to your .gitignore immediately. Also add .env.local, .env.production, etc. if you use them.

Instead of committing the actual values, commit a .env.example with placeholder or fake values. This documents what is needed without exposing anything.

# .env.example
DB_URL=postgres://user:password@localhost:5432/mydb
API_KEY=your-api-key-here
PORT=3000
Enter fullscreen mode Exit fullscreen mode

Use a Validation Library

Manual checks are fine for small projects, but for anything serious use a schema validator like envalid or joi. They give you type coercion, defaults, and clear error messages.

// with envalid
const { cleanEnv, str, num } = require('envalid');

const env = cleanEnv(process.env, {
  DB_URL: str(),
  API_KEY: str(),
  PORT: num({ default: 3000 }),
});

module.exports = env;
Enter fullscreen mode Exit fullscreen mode

This catches missing vars, wrong types, and allows sensible defaults without scattering process.env calls.

Never Log Secrets

It is surprisingly easy to log an env var while debugging. Make a habit of not logging process.env entirely. If you must log config, redact sensitive fields.

function safeConfig(config) {
  const copy = { ...config };
  if (copy.apiKey) copy.apiKey = '***';
  return copy;
}

console.log('Config loaded:', safeConfig(config));
Enter fullscreen mode Exit fullscreen mode

Also be careful with error messages. Some libraries include connection strings in thrown errors. Wrap them to strip credentials.

Use Different Values per Environment

Don't reuse the same API key in dev and prod. A leaked dev key might be less critical, but it is still a foothold. Separate keys per environment make it easier to rotate or revoke one without affecting others.

Use a naming convention: DB_URL_DEV, DB_URL_PROD, or better, use separate .env files and CI secrets per environment. Most platforms (Heroku, Vercel, AWS) have built-in secret management. Use that instead of shipping env vars in code.

Avoid Defaults That Are Real Secrets

A common anti-pattern is setting a default like apiKey: process.env.API_KEY || 'sk_live_1234'. That default is a real secret sitting in your source. Use an empty string or a placeholder that obviously fails if used.

const apiKey = process.env.API_KEY || ''; // will fail when making API calls
Enter fullscreen mode Exit fullscreen mode

If you need a default for local dev, use a fake value that is clearly not real, and ensure your code fails loudly if it tries to use it.

Rotate and Restrict

Treat secrets as perishable. If you suspect a leak, rotate the key. Use short-lived credentials where possible. Many cloud providers offer temporary tokens via IAM roles or service accounts. Prefer those over long-lived keys.

Also restrict permissions. The API key used by your app should only have the minimum scope needed. If it gets leaked, the blast radius is smaller.

Tools and Practices Summary

  • Use a central config module, not raw process.env everywhere.
  • Validate required vars at startup.
  • Keep .env out of git; commit only .env.example.
  • Use a validation library for type safety and defaults.
  • Redact secrets in logs and errors.
  • Separate secrets per environment.
  • No real secrets as defaults.
  • Rotate keys and use minimal permissions.

These habits take a little discipline but save you from embarrassing and costly leaks. Start with the config module and the .gitignore rule; the rest can follow as your project grows.

Top comments (0)