Managing .env and Environment Variables in Node.js

THEJORD Team1 min read
nodejsenvconfigurationsecurity

How to use environment variables in Node.js: dotenv, .env files, security. Best practices.

Managing .env and Environment Variables in Node.js

Introduction to Environment Variables

Environment variables are key-value pairs that configure your application's behavior without hardcoding values in your source code. They're essential for managing secrets, API keys, and environment-specific configuration. This guide covers best practices for using .env files and environment variables in Node.js applications.

Why Use Environment Variables?

Security

  • Keep secrets out of source code
  • Never commit API keys to Git
  • Different credentials per environment

Flexibility

  • Same code in dev, staging, production
  • Easy configuration changes without deployment
  • Container and cloud-native deployment

The .env File

Basic Syntax

# .env file
# Comments start with #

DATABASE_URL=postgres://user:pass@localhost:5432/mydb
API_KEY=sk-abc123def456
PORT=3000
DEBUG=true
NODE_ENV=development

# Strings with spaces need quotes
APP_NAME="My Application"

# Multi-line values
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"

File Naming Conventions

.env                 # Default, loaded in all environments
.env.local           # Local overrides (git-ignored)
.env.development     # Development-specific
.env.production      # Production-specific
.env.test            # Test-specific
.env.example         # Template for required variables

Using dotenv in Node.js

Installation

npm install dotenv

Basic Usage

// Load at the top of your entry file
import 'dotenv/config';
// or
require('dotenv').config();

// Access variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;

Conditional Loading

import dotenv from 'dotenv';

// Load different files based on NODE_ENV
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
dotenv.config({ path: envFile });

// Override with local file
dotenv.config({ path: '.env.local', override: true });

Type Safety with TypeScript

Type Definitions

// types/env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production' | 'test';
    PORT: string;
    DATABASE_URL: string;
    API_KEY: string;
  }
}

Validation with Zod

import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.string().transform(Number),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
});

// Validate and parse
const env = envSchema.parse(process.env);

// Use typed env
console.log(env.PORT); // number
console.log(env.DATABASE_URL); // string

Framework Integration

Next.js

// .env.local (automatically loaded)
DATABASE_URL=postgres://...

// Public variables (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com

// Usage in code
const apiUrl = process.env.NEXT_PUBLIC_API_URL;  // Client-safe
const dbUrl = process.env.DATABASE_URL;           // Server-only

Vite

// .env
VITE_API_URL=https://api.example.com

// Usage (only VITE_ prefixed vars are exposed)
const apiUrl = import.meta.env.VITE_API_URL;

Express

// config.js
import 'dotenv/config';

export const config = {
  port: parseInt(process.env.PORT) || 3000,
  dbUrl: process.env.DATABASE_URL,
  isProduction: process.env.NODE_ENV === 'production',
  logLevel: process.env.LOG_LEVEL || 'info',
};

// app.js
import { config } from './config.js';
app.listen(config.port);

Security Best Practices

.gitignore

# Always ignore .env files with secrets
.env
.env.local
.env.*.local

# Keep example file
!.env.example

.env.example Template

# .env.example (commit this to repo)
# Copy to .env and fill in values

DATABASE_URL=postgres://user:password@localhost:5432/dbname
API_KEY=your-api-key-here
PORT=3000
NODE_ENV=development

Secret Rotation

// Support multiple API keys during rotation
const apiKeys = process.env.API_KEYS?.split(',') || [];

function validateApiKey(key) {
  return apiKeys.includes(key);
}

Production Deployment

Docker

# Dockerfile - don't include .env
FROM node:20
COPY . .
RUN npm install
CMD ["node", "server.js"]

# docker-compose.yml
services:
  app:
    build: .
    env_file:
      - .env.production
    environment:
      - NODE_ENV=production

Cloud Platforms

# Vercel
vercel env add DATABASE_URL production

# Heroku
heroku config:set DATABASE_URL=postgres://...

# AWS (via Parameter Store or Secrets Manager)
aws ssm put-parameter --name "/app/DATABASE_URL" --value "..." --type SecureString

Common Patterns

Default Values

const port = process.env.PORT || 3000;
const host = process.env.HOST ?? 'localhost';
const debug = process.env.DEBUG === 'true';

Required Variables

function requireEnv(name) {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

const apiKey = requireEnv('API_KEY');

Parsing Complex Values

# .env
ALLOWED_ORIGINS=http://localhost:3000,https://example.com
FEATURE_FLAGS={"darkMode":true,"betaFeatures":false}

// JavaScript
const origins = process.env.ALLOWED_ORIGINS?.split(',') || [];
const features = JSON.parse(process.env.FEATURE_FLAGS || '{}');

Tools and Resources

For working with configuration:

Troubleshooting

Variables Not Loading

// Check if dotenv is loaded first
console.log(process.env.MY_VAR); // undefined

// Solution: import dotenv before other imports
import 'dotenv/config';
import { myModule } from './module.js';

Different Values in Production

// Debug: log which file is loaded
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('Loading from:', `.env.${process.env.NODE_ENV}`);

Conclusion

Environment variables are essential for secure, flexible applications. Key takeaways:

  • Never commit secrets to source control
  • Use .env files for local development
  • Validate environment variables at startup
  • Use platform-specific secret management in production
  • Keep .env.example updated for team onboarding

For more developer resources, explore our free online tools. For dotenv documentation, see dotenv on GitHub.

Frequently Asked Questions

What are environment variables for?

To separate configuration (keys, URLs, secrets) from code, so it changes per environment without touching the source.

Why should you not put secrets in the code?

Because they would end up in the repository and be visible to anyone; use environment variables or a secret manager.