cross-env vs dotenv vs env-cmd
Managing Environment Variables in Node.js and Frontend Build Pipelines
cross-envdotenvenv-cmdSimilar Packages:

Managing Environment Variables in Node.js and Frontend Build Pipelines

cross-env, dotenv, and env-cmd are essential utilities for handling environment variables in JavaScript projects, but they solve different parts of the problem. dotenv loads variables from a .env file into process.env at runtime, making them available to your application code. cross-env ensures that setting environment variables in package.json scripts works consistently across Windows, macOS, and Linux. env-cmd acts as a wrapper that loads specific environment files before executing a command, often used to swap configurations for different deployment targets without changing code.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
cross-env06,52420.2 kB1a year agoMIT
dotenv020,519103 kB24 months agoBSD-2-Clause
env-cmd01,81455.2 kB25a year agoMIT

Cross-Env vs Dotenv vs Env-Cmd: Architecture and Use Cases

Managing configuration in JavaScript applications often leads to confusion because cross-env, dotenv, and env-cmd appear to do similar things. In reality, they operate at different stages of the execution lifecycle and solve distinct problems. Understanding where each tool fits prevents architectural bugs, especially when moving from local development to production CI/CD pipelines.

πŸ–₯️ Setting Variables in Scripts: The OS Compatibility Problem

When you write scripts in package.json, you often need to set flags like NODE_ENV. The syntax for doing this differs between operating systems. Unix-based systems (macOS, Linux) allow VAR=value command, while Windows CMD requires set VAR=value && command.

cross-env solves this by providing a unified syntax that works everywhere. It sets the variable in the current process before running your command, ensuring your build tools receive the correct flags regardless of the developer's OS.

// package.json
{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack",
    "start": "cross-env API_PORT=3000 node server.js"
  }
}

dotenv does not handle script-level variable setting. If you try to use it to set NODE_ENV in a script command, it will fail on Windows because dotenv is a library loaded inside JavaScript, not a shell utility.

// package.json (Incorrect usage for setting flags)
{
  "scripts": {
    "build": "dotenv NODE_ENV=production webpack" 
  }
}

env-cmd can set variables, but its primary design is loading files. While it can accept key-value pairs, using it just for simple OS compatibility is often overkill compared to the lightweight cross-env.

// package.json (Possible but less common for simple flags)
{
  "scripts": {
    "build": "env-cmd -e production webpack"
  }
}

πŸ“‚ Loading Files: Runtime vs. Command Wrapper

The core distinction lies in when and how the .env file is loaded.

dotenv is a module you import inside your code. It reads the .env file and populates process.env immediately. This gives you full control over when loading happens, but you must remember to import it at the very top of your entry file.

// server.js
require('dotenv').config(); 
// Or with ES Modules: import 'dotenv/config';

console.log(process.env.DB_PASSWORD); // Available here

env-cmd acts as an executable wrapper. You don't import it in your code. Instead, you run your script through env-cmd, which loads the specified file into the shell environment before your Node process even starts. This keeps your source code clean of loading logic.

# Terminal usage
npx env-cmd -f .env.staging node server.js

// server.js (No import needed)
console.log(process.env.DB_PASSWORD); // Already available

cross-env does not load .env files by default. It only sets variables you explicitly pass to it in the command line. If you need file loading with cross-env, you still need to combine it with dotenv in your code.

// server.js (Must still require dotenv)
require('dotenv').config();

// package.json
// "start": "cross-env PORT=3000 node server.js"

πŸ”„ Swapping Environments: Staging vs. Production

In complex workflows, you often need to swap configuration files based on the target environment (e.g., .env.local, .env.ci, .env.production).

env-cmd excels here. It allows you to define named environments in a config file or simply pass different file paths. This is extremely useful in CI/CD pipelines where you want to enforce a specific file without modifying code.

# Run with specific file
env-cmd -f ./config/staging.env npm run test

# Run with named environment (if configured in .env-cmdrc)
env-cmd -e staging npm run test

dotenv requires you to manually specify the path in your code or pass a path option. This often leads to cluttered entry files or complex conditional logic to decide which file to load.

// server.js
const path = process.env.NODE_ENV === 'production' 
  ? '.env.production' 
  : '.env.local';

require('dotenv').config({ path });

cross-env handles this by setting a variable that your code then uses to decide which file to load (as shown above), or by passing the path directly if combined with other tools. It doesn't natively swap files itself.

# Cross-env sets the variable, code handles the rest
NODE_ENV=staging node server.js

πŸ›‘οΈ Security and Default Behavior

Security practices differ slightly regarding how these tools handle missing files or existing variables.

dotenv by default will not overwrite existing variables in process.env. This is a safety feature to prevent accidentally overriding system-level variables. It also silently fails if the .env file is missing, which can be dangerous if you expect secrets to be present.

// dotenv default behavior
require('dotenv').config(); // Won't crash if .env is missing

// Strict mode (crashes if file missing)
require('dotenv').config({ path: '.env', override: false });

env-cmd throws an error by default if the specified file cannot be found. This is often preferred in production pipelines to fail fast rather than running with missing configuration.

# env-cmd will exit with error code if .env.production is missing
env-cmd -f .env.production node server.js

cross-env simply sets the variable. It has no concept of file security or existence checks because it doesn't read files. It relies entirely on the arguments you provide.

# Sets the variable regardless of context
cross-env SECRET_KEY=123 node server.js

🌐 Real-World Scenarios

Scenario 1: Local Development Setup

You are building a Node.js API. You want developers to just clone the repo and run npm start without worrying about OS differences or manual file loading.

  • βœ… Best Choice: dotenv + cross-env
  • Why? dotenv loads the .env file in code. cross-env ensures the NODE_ENV flag in package.json works for Windows devs.
// package.json
"scripts": {
  "start": "cross-env NODE_ENV=development node src/index.js"
}
// src/index.js
require('dotenv').config();

Scenario 2: CI/CD Pipeline with Multiple Stages

Your pipeline needs to run tests against a specific .env.test file and deploy using .env.prod. You don't want to modify code for each stage.

  • βœ… Best Choice: env-cmd
  • Why? You can explicitly point the command to the correct file in your pipeline config (GitHub Actions, Jenkins) without touching the application code.
# .github/workflows/deploy.yml
steps:
  - name: Deploy Staging
    run: npx env-cmd -f .env.staging npm run deploy
  - name: Deploy Production
    run: npx env-cmd -f .env.prod npm run deploy

Scenario 3: Frontend Build Scripts

You are using Webpack or Vite. You need to pass a mode flag to the bundler. The team uses mixed operating systems.

  • βœ… Best Choice: cross-env
  • Why? Bundlers rely on process.env.NODE_ENV. cross-env is the most reliable way to inject this variable before the bundler starts.
// package.json
"scripts": {
  "build": "cross-env NODE_ENV=production vite build"
}

πŸ“Œ Summary Table

Featurecross-envdotenvenv-cmd
Primary GoalOS-agnostic variable settingLoad .env file in codeExecute command with env file
Execution LevelShell / Process SpawnRuntime (Inside JS)Shell Wrapper
File Loading❌ Noβœ… Yes (.env)βœ… Yes (Any file)
OS Compatibilityβœ… Excellent (Win/Mac/Linux)βœ… N/A (JS Library)βœ… Good (Node based)
Code ChangesNone (package.json only)Requires require()None (CLI only)
Fail on MissingN/A❌ No (Silent)βœ… Yes (Default)

πŸ’‘ Final Recommendation

These tools are not mutually exclusive; they are often used together.

  1. Always use dotenv in your Node.js applications to load local secrets during development. It is the standard for accessing .env variables within code.
  2. Add cross-env to your package.json scripts if your team includes Windows users. It is a small dependency that prevents "works on my machine" issues related to environment flags.
  3. Introduce env-cmd when your deployment strategy requires swapping entire configuration files dynamically (e.g., in Docker containers or CI pipelines) without altering your application's entry point logic.

The Golden Rule: Use cross-env to set flags in package.json, dotenv to load defaults in your code, and env-cmd to override those defaults in automated pipelines.

How to Choose: cross-env vs dotenv vs env-cmd

  • cross-env:

    Choose cross-env when you need to set environment variables directly in your package.json scripts (e.g., NODE_ENV=production) and must support Windows developers. It is the industry standard for solving the syntax incompatibility between Unix shells and Windows CMD/PowerShell. Use it as a prefix for your build or start commands to ensure reliable execution on any OS.

  • dotenv:

    Choose dotenv when your application needs to read configuration values from a .env file into process.env during runtime. It is the foundational tool for local development, allowing you to keep secrets out of your codebase. It is best suited for backend services or Node.js scripts where you control the entry point and can load the file before your app logic runs.

  • env-cmd:

    Choose env-cmd when you need to switch between multiple distinct environment configurations (e.g., staging vs. production) by passing a flag to your CLI commands. It is ideal for CI/CD pipelines or complex monorepos where you want to enforce loading a specific file (like .env.staging) before a script runs, rather than relying on the default .env file or inline variable setting.

README for cross-env

cross-env πŸ”€

Run scripts that set and use environment variables across platforms

πŸŽ‰ NOTICE: cross-env is "done" as in it does what it does and there's no need for new features. Learn more


Build Status Code Coverage version downloads MIT License

The problem

Most Windows command prompts will choke when you set environment variables with NODE_ENV=production like that. (The exception is Bash on Windows, which uses native Bash.) Similarly, there's a difference in how windows and POSIX commands utilize environment variables. With POSIX, you use: $ENV_VAR and on windows you use %ENV_VAR%.

This solution

cross-env makes it so you can have a single command without worrying about setting or using the environment variable properly for the platform. Just set it like you would if it's running on a POSIX system, and cross-env will take care of setting it properly.

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's devDependencies:

npm install --save-dev cross-env

WARNING! Make sure that when you're installing packages that you spell things correctly to avoid mistakenly installing malware

NOTE : Version 8 of cross-env only supports Node.js 20 and higher, to use it on Node.js 18 or lower install version 7 npm install --save-dev cross-env@7

Usage

I use this in my npm scripts:

{
	"scripts": {
		"build": "cross-env NODE_ENV=production node ./start.js --enable-turbo-mode"
	}
}

Ultimately, the command that is executed (using cross-spawn) is:

node ./start.js --enable-turbo-mode

The NODE_ENV environment variable will be set by cross-env

You can set multiple environment variables at a time:

{
	"scripts": {
		"build": "cross-env FIRST_ENV=one SECOND_ENV=two node ./my-program"
	}
}

You can also split a command into several ones, or separate the environment variables declaration from the actual command execution. You can do it this way:

{
	"scripts": {
		"parentScript": "cross-env GREET=\"Joe\" npm run childScript",
		"childScript": "cross-env-shell \"echo Hello $GREET\""
	}
}

Where childScript holds the actual command to execute and parentScript sets the environment variables to use. Then instead of run the childScript you run the parent. This is quite useful for launching the same command with different env variables or when the environment variables are too long to have everything in one line. It also means that you can use $GREET env var syntax even on Windows which would usually require it to be %GREET%.

If you precede a dollar sign with an odd number of backslashes the expression statement will not be replaced. Note that this means backslashes after the JSON string escaping took place. "FOO=\\$BAR" will not be replaced. "FOO=\\\\$BAR" will be replaced though.

Lastly, if you want to pass a JSON string (e.g., when using ts-loader), you can do as follows:

{
	"scripts": {
		"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} node some_file.test.ts"
	}
}

Pay special attention to the triple backslash (\\\) before the double quotes (") and the absence of single quotes ('). Both of these conditions have to be met in order to work both on Windows and UNIX.

cross-env vs cross-env-shell

The cross-env module exposes two bins: cross-env and cross-env-shell. The first one executes commands using cross-spawn, while the second one uses the shell option from Node's spawn.

The main use case for cross-env-shell is when you need an environment variable to be set across an entire inline shell script, rather than just one command.

For example, if you want to have the environment variable apply to several commands in series then you will need to wrap those in quotes and use cross-env-shell instead of cross-env.

{
	"scripts": {
		"greet": "cross-env-shell GREETING=Hi NAME=Joe \"echo $GREETING && echo $NAME\""
	}
}

The rule of thumb is: if you want to pass to cross-env a command that contains special shell characters that you want interpreted, then use cross-env-shell. Otherwise stick to cross-env.

On Windows you need to use cross-env-shell, if you want to handle signal events inside of your program. A common case for that is when you want to capture a SIGINT event invoked by pressing Ctrl + C on the command-line interface.

Windows Issues

Please note that npm uses cmd by default and that doesn't support command substitution, so if you want to leverage that, then you need to update your .npmrc to set the script-shell to powershell. Learn more here.

Inspiration

I originally created this to solve a problem I was having with my npm scripts in angular-formly. This made contributing to the project much easier for Windows users.

Other Solutions

  • env-cmd - Reads environment variables from a file instead
  • @naholyr/cross-env - cross-env with support for setting default values

LICENSE

MIT