Last updated: 2026-09-14

Environment Variables

An environment variable is a value that lives outside your code, set in the environment a program runs in, and made available to that program at runtime. In Node.js, environment variables are the standard way to configure an application differently across different contexts — development, testing, production — without changing a single line of code.

They are also the standard way to keep sensitive information — API keys, database credentials, secret tokens — out of your source code entirely.


Why Environment Variables Matter

Hardcoding configuration directly into application code creates two real problems.

The first is that the same code often needs to behave differently depending on where it runs. A database connection string pointing to a local development database is different from the one pointing to a production database. If that value is hardcoded, switching between environments means editing and re-editing the source code itself — fragile, error-prone, and easy to get wrong at the worst possible moment, like accidentally deploying with a development database connection.

The second, more serious problem is security. Secrets — API keys, database passwords, authentication tokens — should never appear directly in source code, because source code is typically committed to version control. A secret committed to a Git repository, even briefly, remains in that repository's history indefinitely, accessible to anyone with access to the repository, including in a public GitHub repository, anyone in the world.

Environment variables solve both problems. Configuration lives outside the codebase, can differ freely between environments, and never needs to be written into a file that gets committed to version control.


Accessing Environment Variables in Node.js

Node.js exposes environment variables through process.env, a global object where every key corresponds to an environment variable name, and every value is a string.

console.log(process.env.NODE_ENV);
console.log(process.env.DATABASE_URL);
console.log(process.env.PORT);

If a variable has not been set in the environment, accessing it returns undefined:

console.log(process.env.SOME_UNDEFINED_VAR); // undefined

Setting Variables When Running a Script

Environment variables can be set directly on the command line when starting a Node.js process:

NODE_ENV=production node server.js

Multiple variables can be set the same way:

NODE_ENV=production PORT=8080 node server.js

On Windows, the syntax differs slightly depending on the shell — this is one of the reasons tools like cross-env (covered later in this topic) exist, to provide a consistent syntax across operating systems.


Every Value in process.env Is a String

This is a detail that catches many developers off guard. Regardless of what the value conceptually represents, process.env stores and returns every value as a string — there is no automatic type conversion.

PORT=3000 node server.js
console.log(process.env.PORT);          // '3000' — a string, not a number
console.log(typeof process.env.PORT);   // 'string'
console.log(process.env.PORT === 3000); // false

If a numeric value is needed, it must be converted explicitly:

const port = Number(process.env.PORT) || 3000;

The same applies to boolean-like values:

DEBUG=true node server.js
console.log(process.env.DEBUG);            // 'true' — still a string
console.log(process.env.DEBUG === true);   // false — comparing string to boolean
console.log(process.env.DEBUG === 'true'); // true — correct comparison

.env Files and dotenv

Setting environment variables manually on the command line every time you run a project is impractical, especially with several variables. The standard solution is a .env file — a plain text file listing key-value pairs — combined with a package that loads it into process.env automatically.

The .env File Format

# .env
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk_test_abc123xyz
DEBUG=true

No quotes are needed around values unless the value itself contains spaces. Comments start with #.

Loading It with dotenv

The dotenv package is the standard tool for loading a .env file's contents into process.env. Install it first:

npm install dotenv

Then, at the very top of your application's entry file, before any other code that might depend on environment variables:

require('dotenv').config();

console.log(process.env.DATABASE_URL); // now available

dotenv.config() reads the .env file in the current working directory, parses it, and assigns each key-value pair to process.env. This must run before anything else in the application reads from process.env, which is why it is placed at the very top of the entry point.

Native .env Support in Node.js

Since Node.js 20.6.0, Node.js can load .env files natively, without installing dotenv at all, using the --env-file flag:

node --env-file=.env server.js

This covers the basic case of loading a .env file, though dotenv remains widely used for its additional features, broader compatibility with older Node.js versions, and ecosystem familiarity. For new projects on a recent Node.js version, the native flag is a reasonable choice if no additional features are needed.


.env Must Never Be Committed to Version Control

A .env file containing real secrets — database passwords, API keys — should never be committed to a Git repository. The standard practice is to add .env to .gitignore:

# .gitignore
.env
.env.local
node_modules/

Instead, a .env.example file is typically committed to the repository, listing the required variable names without their actual values, so that anyone setting up the project knows what needs to be configured:

# .env.example
NODE_ENV=
PORT=
DATABASE_URL=
API_KEY=

This is a standard, expected convention across the Node.js ecosystem — a new contributor to a project copies .env.example to .env and fills in their own values, without the real secrets ever being shared through version control.


Multiple Environment Files

Larger projects often use separate .env files for different environments:

FilePurpose
.envDefault values, loaded in every environment
.env.developmentOverrides for local development
.env.testOverrides used when running automated tests
.env.productionOverrides for the production deployment

Loading the correct file based on the current environment typically depends on NODE_ENV:

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`,
});

Many frameworks, including Next.js, handle this convention automatically without requiring manual configuration — Next.js loads .env.local, .env.development, and .env.production based on context without any setup, which is covered in detail in the Next.js research series.


NODE_ENV

NODE_ENV is the single most widely recognised environment variable in the Node.js ecosystem. It is not built into Node.js itself with any special behaviour — it is simply a convention that the vast majority of libraries and frameworks check and respond to.

if (process.env.NODE_ENV === 'production') {
  // disable verbose logging, enable caching, etc.
}

Typical values are development, test, and production. Many libraries change their behaviour automatically based on this value — Express, for example, disables certain debug-friendly error pages when NODE_ENV is set to production. Setting NODE_ENV correctly in each environment is one of the most consequential single configuration decisions in a Node.js deployment, because so much downstream behaviour depends on it.


Cross-Platform Environment Variable Syntax — cross-env

Setting environment variables directly on the command line has different syntax on different operating systems:

# macOS/Linux
NODE_ENV=production node server.js

# Windows Command Prompt
set NODE_ENV=production && node server.js

This inconsistency becomes a real problem when defining npm scripts that need to run identically across every team member's machine, regardless of operating system. The cross-env package solves this by providing a single, OS-independent syntax:

npm install --save-dev cross-env
{
  "scripts": {
    "start": "cross-env NODE_ENV=production node server.js"
  }
}

With cross-env, the same script command works correctly whether it runs on macOS, Linux, or Windows.


Validating Required Environment Variables

A common source of confusing runtime failures is an application that silently continues running with a missing or malformed environment variable, only to fail unpredictably later when that variable is actually used. A more robust approach validates required variables immediately at startup:

const requiredEnvVars = ['DATABASE_URL', 'API_KEY', 'PORT'];

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

Failing fast and loudly at startup — rather than allowing the application to start in a broken, partially-configured state — makes configuration problems immediately visible instead of surfacing as confusing errors deep inside unrelated code later on.


Summary

TaskHow
Read an environment variableprocess.env.VARIABLE_NAME
Set a variable for one commandVARIABLE_NAME=value node server.js
Load variables from a .env filerequire('dotenv').config()
Load .env without a package (Node.js 20.6+)node --env-file=.env server.js
Keep secrets out of version controlAdd .env to .gitignore; commit .env.example instead
Check the current environmentprocess.env.NODE_ENV === 'production'
Consistent syntax across operating systemsUse cross-env in npm scripts
Convert a string value to a numberNumber(process.env.PORT)