Course:Internet & Tools/
Lesson

Here is a pattern that happens every single day: a developer asks Claude or Copilot to "set up a database connection" and gets back perfectly working code with the password right there in the file. They commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. it, push it, and now their database credentials are in their git history permanently. The .env file pattern exists specifically to prevent this, it gives secrets a home that git never touches.

What dotenvWhat is dotenv?An npm package that reads a .env file and loads its key-value pairs into process.env at startup. does

The dotenv package reads a .env file in your project root and injects each key-value pair into process.env before your app runs. One call at the top of your entry file, and the restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources. of your code reads process.env as if you had exported everything by hand.

The AI pattern vs the correct pattern

// What Copilot generates - secrets baked into source code
const stripe = require('stripe')('sk_live_4eC39HqLyjWDarjtT1zdp7dc');
const db = require('pg').Pool({
  connectionString: 'postgres://admin:P@ssw0rd!@db.example.com:5432/prod'
});

// What you should actually write - secrets loaded from .env
require('dotenv').config();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const db = require('pg').Pool({
  connectionString: process.env.DATABASE_URL
});
Every time ChatGPT, Copilot, or Claude generates code that connects to an external service, immediately check whether it hardcoded the credentials. It almost certainly did. Replace the string literals with process.env reads and put the real values in your .env file.

Setting it up

npm install dotenv

Create a .env file in your project root (next to package.json):

# .env - never commit this file
PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
STRIPE_SECRET_KEY=sk_test_1234567890
JWT_SECRET=a-long-random-string-here
NODE_ENV=development

Then load it at the very top of your main file:

// CommonJS (index.js or server.js)
require('dotenv').config();

// ES Modules
import 'dotenv/config';

// Now process.env is populated
const port = process.env.PORT; // '3000'
const dbUrl = process.env.DATABASE_URL;
02

.env file format rules

The format is straightforward but has a few tricky edges that trip people up:

# Lines starting with # are comments
KEY=value
KEY_WITH_SPACES="hello world"     # Quotes required for values with spaces

# No spaces around the equals sign
WRONG = value    # dotenv will NOT parse this correctly
RIGHT=value      # Correct

# Multiline values need double quotes
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAK...
-----END RSA PRIVATE KEY-----"

# Reference other variables (dotenv-expand package)
BASE_URL=https://api.example.com
USERS_URL=${BASE_URL}/users
RuleCorrectIncorrect
No spaces around =KEY=valueKEY = value
Quote values with spacesMSG="hello world"MSG=hello world
Comments with ## This is a commentWorks on its own line
Empty valuesKEY=Sets to empty string
No export keywordKEY=valueexport KEY=value (not needed)
03

Multiple .env files

As projects grow, you will want different configs for different contexts. Most frameworks follow this convention:

FilePurposeCommit to git?
.envShared defaultsNo
.env.localYour personal overridesNo
.env.developmentDev-specific valuesOptional
.env.testTest environment valuesOptional
.env.productionProduction defaultsNo
.env.exampleTemplate for teammatesYes

When multiple files are loaded (as frameworks like Next.js and Vite do automatically), more specific files override less specific ones. Your .env.local always wins over .env.

04

Protect your secrets in git

The first thing to do after creating a .env file is add it to .gitignore:

gitignore
# Environment variables - never commit these
.env
.env.local
.env.*.local
.env.production

# But DO commit the example file
!.env.example

Then create a .env.example that documents every variable your project needs, with safe placeholders instead of real secrets:

# .env.example - commit this, share it with your team
PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
STRIPE_SECRET_KEY=
JWT_SECRET=
NODE_ENV=development

When a new teammate clones the repo, they copy .env.example to .env and fill in the real values. No secrets travel through git.

If you accidentally commit a .env file, changing .gitignore afterward will not remove it from history. You must rotate (invalidate and replace) every leaked credential immediately, then use git filter-repo to scrub the file from history. The secret should be considered compromised the moment it was pushed.
05

Debugging dotenvWhat is dotenv?An npm package that reads a .env file and loads its key-value pairs into process.env at startup.

If your variables are not loading, dotenv's return value tells you why:

const result = require('dotenv').config();

if (result.error) {
  console.error('Failed to load .env:', result.error.message);
  // Common causes: .env file not found, wrong working directory
} else {
  console.log('Loaded variables:', Object.keys(result.parsed));
}

Common dotenv issues:

ProblemCauseFix
Variables are undefineddotenv loaded after code that reads themMove require('dotenv').config() to the very first line
.env file not foundRunning from wrong directoryCheck process.cwd() or use { path: '.env' } with absolute path
Values have quotes in themUsing KEY="value" with some parsersRemove quotes if the value has no spaces
Changes not taking effectProcess not restartedRestart your dev server after editing .env
06

Quick reference

TaskCode / Command
Install dotenvnpm install dotenv
Load in CommonJSrequire('dotenv').config()
Load in ESMimport 'dotenv/config'
Load custom pathrequire('dotenv').config({ path: '.env.staging' })
Add to gitignore.env and .env.*.local
Document variablesCreate .env.example with empty values
Debug loadingCheck require('dotenv').config().error