| |

npm 12 ๐ŸŒ Environment Variables in npm Scripts

โฑ๏ธ Estimated read: 13 min | |

Environment variables are the mechanism by which npm scripts receive configuration that varies by machine, by environment, or by deployment target. A script that reads process.env.NODE_ENV or process.env.API_URL is not hardcoded to a single setup; it adapts based on what the surrounding shell or .env file provides. The complication is that setting environment variables in a script is not portable. The syntax that works in Bash (VAR=value command) fails in Windows cmd.exe, and the syntax that works in PowerShell ($env:VAR="value") fails everywhere else . This chapter covers the three approaches to providing variables to npm scriptsโ€”inline assignment, .env files loaded by the script, and cross-platform wrappersโ€”and the trade-offs that determine which one belongs in which situation.

Key point: Inline environment variable assignment in package.json scripts is POSIX-only. If the project is developed or CI-built on Windows, the script must use cross-env or a .env loader, or the build will fail on one platform and succeed on the other.


Why environment variables in npm scripts matter

The portability problem. A package.json is shared across every developer’s machine and every CI runner. If a script contains NODE_ENV=production node build.js, it works on macOS and Linux and fails on Windows cmd.exe with “'NODE_ENV' is not recognized as an internal or external command.” The failure is not in the code; it is in the shell syntax. cross-env exists specifically to translate POSIX-style assignment into the correct syntax for the platform running the script .

The secret management problem. Database URLs, API keys, and tokens should not be committed to package.json or source control. A .env file kept outside version control provides a place for these values, and a loader such as dotenv reads the file into process.env at runtime . The script references process.env.DATABASE_URL; the value lives in a file that is gitignored.

The environment switching problem. A project typically needs different configuration in development, test, and production. One approach is to set NODE_ENV inline and let the application read it to select a configuration object. Another is to maintain .env.development, .env.test, and .env.production files and load the appropriate one based on an inline flag . Both approaches rely on npm scripts passing the discriminatorโ€”NODE_ENV or a similar variableโ€”into the process.

The CI reproducibility problem. A CI runner may have environment variables set at the system level, injected by the CI platform, or absent entirely. Scripts that assume a variable exists fail in CI with no clear error. Scripts that provide a default (process.env.PORT || 3000) are more resilient. Scripts that fail fast with a clear message when a required variable is missing are more debuggable. The choice between defaulting and failing is part of the design.


a. Inline Assignment and the Portability Failure

The simplest way to set an environment variable for a single script is to prefix the command with the assignment. On POSIX systems, VAR=value command sets VAR for that command’s duration and does not leak it into the parent shell. In package.json, this is how NODE_ENV is set for build and test scripts.

{
  "scripts": {
    "build": "NODE_ENV=production node build.js",
    "test": "NODE_ENV=test vitest"
  }
}

This works on macOS, Linux, and in CI runners that use a POSIX shell. It fails on Windows cmd.exe and PowerShell, where the syntax is different. The error is immediate and the cause is not always obvious to a developer who has only used one platform. The cross-env package exists to eliminate this class of failure .

{
  "scripts": {
    "build": "cross-env NODE_ENV=production node build.js"
  }
}

The cross-env wrapper reads the POSIX-style assignment, detects the platform, and executes the command with the variable set correctly for that platform. It works on Windows cmd.exe, PowerShell, and POSIX shells without modification to the script . The package should be installed as a devDependency because it is only needed at development and build time, not at runtime.

Multiple variables can be set in one invocation. The assignments precede the command, and all of them are available to the process.

{
  "scripts": {
    "build:staging": "cross-env NODE_ENV=staging API_URL=https://staging.example.com node build.js"
  }
}

For commands that require shell interpretation of the variablesโ€”pipes, &&, or variable expansion inside a quoted command stringโ€”cross-env-shell is the appropriate variant. It spawns the command through a shell rather than executing it directly, which allows shell syntax to work on Windows as well . The rule of thumb is to use cross-env for simple commands and cross-env-shell only when shell features are required.


b. .env Files and Loaders

A .env file stores key-value pairs outside the codebase. The format is simple: one assignment per line, with no quoting required for most values. A loader reads the file and assigns the values to process.env before the application logic runs.

# .env โ€” never commit this file
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk_test_abc123
LOG_LEVEL=debug

The dotenv package is the most common loader. It is installed as a dependency and invoked at the top of the application entry point, before any code that reads process.env.

// src/index.js โ€” load .env before anything else
import 'dotenv/config';

// Now process.env.DATABASE_URL is available
const dbUrl = process.env.DATABASE_URL;

The dotenv/config import loads .env from the current working directory automatically. For explicit control over the path, the function form is used.

import dotenv from 'dotenv';
dotenv.config({ path: './config/.env.local' });

The .env file must be added to .gitignore. Committing it exposes secrets in the repository history, where they remain even after the file is deleted. A common pattern is to commit a .env.example file with placeholder values and no secrets, so that new contributors know which variables are required.

# .env.example โ€” committed to version control
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=your_key_here

Node.js itself gained native .env file support via the --env-file flag, which eliminates the need for dotenv in some cases. The flag loads the specified file before executing the script .

{
  "scripts": {
    "start": "node --env-file=.env src/index.js"
  }
}

The native flag is available in Node.js 20.6 and later. For projects that support older Node versions or need features like variable expansion inside the .env file, dotenv remains necessary.


c. Combining Inline Flags with .env Files

A common pattern is to use inline assignment to select which .env file to load, then let the loader read the selected file. This keeps the discriminator (NODE_ENV) in the script while keeping the secrets out of it.

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development node --env-file=.env.development src/index.js",
    "start": "cross-env NODE_ENV=production node --env-file=.env.production src/index.js"
  }
}

The application reads NODE_ENV to decide which configuration path to take, and the loader provides the values for that environment. The .env.development and .env.production files are both gitignored, and both have committed .example counterparts.

When dotenv is used instead of the native flag, the path is selected in code rather than in the script.

// src/config.js โ€” select .env file based on NODE_ENV
import dotenv from 'dotenv';

const envFile = process.env.NODE_ENV === 'production'
  ? '.env.production'
  : '.env.development';

dotenv.config({ path: envFile });

This approach places the selection logic in JavaScript rather than in the package.json script. It is more verbose but allows the selection to depend on multiple factors, not just a single inline variable.


Complete Example Session

// ============================================
// PART 1: THE PROBLEM โ€” POSIX-ONLY SCRIPT
// ============================================
// package.json โ€” works on macOS/Linux, fails on Windows
{
  "scripts": {
    "build": "NODE_ENV=production node build.js"
  }
}


// ============================================
// PART 2: CROSS-ENV FIX
// ============================================
// Install cross-env as a dev dependency
// npm install --save-dev cross-env
{
  "scripts": {
    "build": "cross-env NODE_ENV=production node build.js"
  }
}


// ============================================
// PART 3: MULTIPLE VARIABLES
// ============================================
{
  "scripts": {
    "build:staging": "cross-env NODE_ENV=staging API_URL=https://staging.example.com node build.js"
  }
}


// ============================================
// PART 4: SHELL-DEPENDENT COMMANDS
// ============================================
// Use cross-env-shell when the command needs shell features
{
  "scripts": {
    "greet": "cross-env-shell GREETING=Hi \"echo $GREETING && node app.js\""
  }
}


// ============================================
// PART 5: .ENV FILE FOR SECRETS
// ============================================
// .env โ€” gitignored
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk_test_abc123


// ============================================
// PART 6: LOADING WITH DOTENV
// ============================================
// src/index.js
import 'dotenv/config';
const dbUrl = process.env.DATABASE_URL;


// ============================================
// PART 7: NODE NATIVE .ENV LOADING
// ============================================
// Node 20.6+ supports --env-file
{
  "scripts": {
    "start": "node --env-file=.env src/index.js"
  }
}


// ============================================
// PART 8: PER-ENVIRONMENT .ENV FILES
// ============================================
{
  "scripts": {
    "dev": "cross-env NODE_ENV=development node --env-file=.env.development src/index.js",
    "start": "cross-env NODE_ENV=production node --env-file=.env.production src/index.js"
  }
}


// ============================================
// PART 9: GITIGNORE THE SECRETS
// ============================================
// .gitignore
.env
.env.*
!.env.example


// ============================================
// PART 10: DEFAULT VALUES IN CODE
// ============================================
// src/config.js โ€” resilient to missing variables
const port = process.env.PORT || 3000;
const logLevel = process.env.LOG_LEVEL || 'info';

The ten parts trace the progression from the portability failure through cross-env, multiple variables, shell-dependent commands, .env files, dotenv, the native --env-file flag, per-environment files, gitignore rules, and default values in code.


Quick Reference

Approaches Compared

ApproachPortabilitySecrets in RepoBest For
Inline VAR=valuePOSIX onlyNoSimple projects, Linux CI
cross-envAll platformsNoCross-platform teams
.env + dotenvAll platformsNo (gitignored)Local secrets
node --env-fileNode 20.6+No (gitignored)Modern Node, no dependency

cross-env Usage

ScenarioCommand
Single variablecross-env NODE_ENV=production node app.js
Multiple variablescross-env A=1 B=2 node app.js
Shell features neededcross-env-shell "echo $VAR && node app.js"
Installnpm install --save-dev cross-env

.env Loading

MethodCode
dotenv autoimport 'dotenv/config'
dotenv explicitdotenv.config({ path: '.env.local' })
Node nativenode --env-file=.env app.js

Best Practices

โœ… Do This:

// Use cross-env for cross-platform compatibility
"build": "cross-env NODE_ENV=production node build.js"                // โœ…
# Add .env to .gitignore
echo ".env" >> .gitignore                                              # โœ…
# Commit .env.example with placeholders
# .env.example โ€” no real secrets                                       # โœ…
// Provide defaults for optional variables
const port = process.env.PORT || 3000;                                 // โœ…
// Use native --env-file on Node 20.6+
"start": "node --env-file=.env src/index.js"                           // โœ…

โŒ Don’t Do This:

// POSIX-only inline assignment in a shared package.json
"build": "NODE_ENV=production node build.js"                           // โŒ
# Commit .env with real secrets
git add .env                                                           // โŒ
// Read variables without defaults or validation
const port = process.env.PORT;                                         // โŒ
// Put secrets directly in package.json scripts
"start": "API_KEY=sk_live_abc123 node app.js"                          // โŒ
// Use cross-env-shell for simple commands
"build": "cross-env-shell NODE_ENV=production node build.js"           // โŒ (overhead)

Common Pitfalls

PitfallWhy It HappensFix
Script fails on WindowsInline VAR=value syntaxUse cross-env
Variable is undefined at runtime.env not loaded or wrong pathCheck loader invocation and path
Secrets exposed in git history.env committedAdd to .gitignore, rotate secrets
CI build fails with missing variableNo default and variable not setProvide default or CI secret
dotenv loads too lateImported after code that reads process.envImport at entry point, before other imports
cross-env-shell hangs on Ctrl+CShell behavior on WindowsUse cross-env for simple commands

Real-World Examples

1. Cross-Platform Build

"build": "cross-env NODE_ENV=production node build.js"

2. Multiple Variables

"build:staging": "cross-env NODE_ENV=staging API_URL=https://staging.example.com node build.js"

3. Load .env with dotenv

import 'dotenv/config';

4. Native Node .env Loading

"start": "node --env-file=.env src/index.js"

5. Per-Environment Files

"dev": "cross-env NODE_ENV=development node --env-file=.env.development src/index.js"

6. Default Port

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

7. Gitignore Secrets

echo ".env" >> .gitignore

8. Example File

# .env.example
API_KEY=your_key_here

9. Shell-Dependent Command

"greet": "cross-env-shell GREETING=Hi \"echo $GREETING && node app.js\""

10. Fail Fast on Missing Variable

if (!process.env.API_KEY) throw new Error('API_KEY required');

Visual

The Portability Failure

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  package.json                                                โ”‚
โ”‚  "build": "NODE_ENV=production node build.js"                โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚  โ”‚ macOS / Linux   โ”‚        โ”‚ Windows cmd.exe โ”‚             โ”‚
โ”‚  โ”‚                 โ”‚        โ”‚                 โ”‚             โ”‚
โ”‚  โ”‚ VAR=value       โ”‚        โ”‚ set VAR=value   โ”‚             โ”‚
โ”‚  โ”‚ command         โ”‚        โ”‚ command         โ”‚             โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚           โ”‚                          โ”‚                       โ”‚
โ”‚           โ–ผ                          โ–ผ                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚  โ”‚ SUCCESS         โ”‚        โ”‚ FAILURE         โ”‚             โ”‚
โ”‚  โ”‚                 โ”‚        โ”‚ 'NODE_ENV' is   โ”‚             โ”‚
โ”‚  โ”‚                 โ”‚        โ”‚ not recognized  โ”‚             โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚                                                              โ”‚
โ”‚  The script is identical. The shell is different.            โ”‚
โ”‚  cross-env translates the assignment for the platform.       โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

cross-env Translation

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  "cross-env NODE_ENV=production node build.js"               โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ cross-env reads โ”‚                                        โ”‚
โ”‚  โ”‚ POSIX syntax    โ”‚                                        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚                                                  โ”‚
โ”‚           โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                   โ”‚
โ”‚           โ–ผ              โ–ผ              โ–ผ                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”‚
โ”‚  โ”‚ Linux/macOS โ”‚ โ”‚ Windows cmd โ”‚ โ”‚ PowerShell  โ”‚          โ”‚
โ”‚  โ”‚             โ”‚ โ”‚             โ”‚ โ”‚             โ”‚          โ”‚
โ”‚  โ”‚ NODE_ENV=   โ”‚ โ”‚ set NODE_   โ”‚ โ”‚ $env:NODE_  โ”‚          โ”‚
โ”‚  โ”‚ production  โ”‚ โ”‚ ENV=prod... โ”‚ โ”‚ ENV="prod"  โ”‚          โ”‚
โ”‚  โ”‚ node build  โ”‚ โ”‚ node build  โ”‚ โ”‚ node build  โ”‚          โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ”‚
โ”‚                                                              โ”‚
โ”‚  One script, three platforms, no conditional logic.          โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

.env Loading Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  .env file (gitignored)                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚ DATABASE_URL=postgresql://...           โ”‚                โ”‚
โ”‚  โ”‚ API_KEY=sk_test_abc123                  โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚                     โ”‚                                        โ”‚
โ”‚                     โ–ผ                                        โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚ Loader (dotenv or --env-file)           โ”‚                โ”‚
โ”‚  โ”‚ Reads file, assigns to process.env      โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚                     โ”‚                                        โ”‚
โ”‚                     โ–ผ                                        โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚ Application code                        โ”‚                โ”‚
โ”‚  โ”‚ process.env.DATABASE_URL                โ”‚                โ”‚
โ”‚  โ”‚ process.env.API_KEY                     โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚                                                              โ”‚
โ”‚  The script does not contain the secret.                     โ”‚
โ”‚  The file does, and the file is not committed.               โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Inline Flag + .env Combination

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  "cross-env NODE_ENV=production node --env-file=.env.prod"   โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ NODE_ENV        โ”‚  Set by cross-env (inline)             โ”‚
โ”‚  โ”‚ =production     โ”‚  Selects behavior in code              โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚                                                  โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ --env-file      โ”‚  Loads secrets from file               โ”‚
โ”‚  โ”‚ .env.production โ”‚  DATABASE_URL, API_KEY, etc.           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚                                                  โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Application     โ”‚  Both sources merged into              โ”‚
โ”‚  โ”‚ process.env     โ”‚  process.env                           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  Inline: the discriminator (which environment).              โ”‚
โ”‚  File: the secrets (what changes per environment).           โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Inline assignmentVAR=value command โ€” POSIX only
Cross-platform wrappercross-env VAR=value command
Shell-dependent variantcross-env-shell "command"
.env loader (package)dotenv โ€” import 'dotenv/config'
.env loader (native)node --env-file=.env (Node 20.6+)
Secrets protectionAdd .env to .gitignore
Example file.env.example with placeholders, committed
Default valuesprocess.env.PORT || 3000
Per-environmentNODE_ENV=production node --env-file=.env.production

Key takeaways:

  • Inline assignment is not portable. NODE_ENV=production node app.js works in POSIX shells and fails in Windows cmd.exe. cross-env translates the assignment for the platform running the script .
  • cross-env versus cross-env-shell. Use cross-env for simple commands. Use cross-env-shell only when the command contains shell featuresโ€”pipes, &&, or variable expansion inside a quoted string .
  • .env files keep secrets out of package.json. The file is gitignored, the script references process.env.VAR, and a loader reads the file before the application runs. The native --env-file flag in Node 20.6+ eliminates the dotenv dependency for simple cases .
  • The discriminator and the secrets can live in different places. NODE_ENV can be set inline via cross-env to select which .env file to load. The environment is in the script; the secrets are in the gitignored file.
  • Provide defaults or fail fast. process.env.PORT || 3000 prevents a missing optional variable from crashing the process. For required variables, an explicit check with a clear error message is better than a undefined that surfaces later.

Remember: Environment variables in npm scripts are a portability problem disguised as a configuration problem. The values themselves are simple; the mechanism for passing them differs by shell, by platform, and by Node version. cross-env solves the shell difference. .env files solve the secret management difference. The native --env-file flag solves the dependency difference for modern Node. The remaining decision is which variables belong in the script (the discriminator) and which belong in the file (the secrets). Keeping that boundary clear is what makes a package.json that works on every developer’s machine and every CI runner without modification.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!