Mastering Node.js Env Variables in Minutes
Posted on: September 01 2025

Environment variables are the backbone for configuring and securing Node.js based apps. With so many modern conventions and techniques, it's easy to forget the basics of how Node.js handles environment variables.
Having a clear understanding of how to use environment variables in Node.js can prevent unexpected issues and security risks.
In this guide I'll be covering some quick essential steps to help master the concept of using environment variables, including:
- How Node.js loads environment variables.
- How and when to use dotenv.
- How to maximize environment flows using dotenv-flow.
- How Node.js version 20 handles environment variables.
- Launching tests with environment variable support.
This guide will provide a quick recap on how to use environment variables with confidence.
Let's get started.
Node.js Environment variables
Let's start by identifying the supported environments in Node.js.
- development
- production
- test
It's encouraged to use these standard environments because introducing custom environments could introduce inconsistencies.
Setting a Node.js environment
To set the environment in Node.js, use the designated variable key NODE_ENV
and define it before executing the node
runtime.
NODE_ENV=production node app.js
Any additional variables can also be added as needed.
NODE_ENV=production DB_PORT=5432 node app.js
Or, using NPM scripts
package.json
{
"scripts": {
"dev": "DB_HOST=http://localhost:7732 NODE_ENV=development node app.js",
"start": "NODE_ENV=production node app.js"
}
}
Any command line environment variables added before the node
start, will be added to the process.env
object in the Node.js runtime.
These values are accessible inside node modules as follows.
Accessing process.env variables
const isProduction = process.env.NODE_ENV === 'production'
const DB_HOST_URL = process.env.DB_HOST || 'http://localhost:5432'
Node.js environment config files
Using environment config files, also known as .env
files, are an easy way to load different variables for different NODE_ENV
environments.
The only catch is that Node.js will not load any of these environment config files automatically.
Why doesn't Node.js automatically load .env files?
Node.js is unopinionated and low-level by design. It let's developers determine how environment variables should be set at runtime.
Files like .env
, .env.local
, etc., are conventions created by the community and not part of the Node.js specification until recently.
Node.js Version 20 supports .env files
Since the release of Node.js version 20, it now supports the --env-file
flag to load .env
files.
What I like about this approach is that the flag is explicitly set as follows. This eliminates any complexity and is sufficient in most cases.
package.json
{
"scripts": {
"dev": "NODE_ENV=development node --env-file .env.development.local app.js",
"start:app": "NODE_ENV=production node --env-file .env.production app.js"
}
}
Unfortunately most teams might not be using Node.js version 20 yet, which leads developers to find alternative solutions to load variables.
Different types of .env files
Over the years, additional .env
files were used to add flexibility and base default values for different environments. Larger development teams will benefit the most but this has become a new standard in many ways.
Examples of different .env
files.
- .env
- .env.local
- .env.development.local
- .env.development
- .env.production.local
- .env.production
- .env.test
Here's a breakdown of the general use cases when working with multiple scopes of environment config files.
File | Committed? | Recommended Use |
---|
.env | ✅ Yes | Default values common to all environments. |
.env.local | ❌ No | Secrets and local-only overrides. Do NOT commit. |
.env.development | ✅ Yes | Development-specific config shared across team. |
.env.development.local | ❌ No | Developer’s local overrides during development. |
.env.production | ✅ Yes | Production defaults (safe to commit). |
.env.production.local | ❌ No | Production-only secrets (API keys, DB creds). |
These files are typically placed in the root of an NPM project.
For example:
Project structure
└── <project-root>
├── app.js
├── .env
├── .env.local
├── .env.production
├── .env.test
└── package.json
Strategies for Development
This is a typical strategy for a local development environment setup.
✅ Commit:
❌ Ignore:
- .env.local
- .env.development.local
Use .env.local
and .env.development.local
for any secret key overrides. Never put secrets in .env
or .env.development
.
Strategies for Production
Here's the general usage for production environment config files.
✅ Commit:
- .env.production (only non-sensitive config values)
❌ Ignore:
It's always more preferable to manage production secrets via:
- CI/CD environment variables
- .env.production.local (only if necessary on the server, not committed to git)
- Secret managers (AWS Secrets Manager, HashiCorp Vault, etc.)
Should I commit .env files into my repo?
Using config files in a strategic way can allow for certain base environment files to be checked in securely.
By not committing .env.local
and .env.*.local
files and strictly using them for local config settings, can offer some flexibility.
Here's an example of how you can omit them from your git repo.
.gitignore
.env.local
.env.*.local
General tip:
- Avoid saving any sensitive data in the base
.env
config file.
- Avoid saving .
env.local
and .env.*.local
files into your git repo.
- Use a .gitignore to omit them.
Using third party .env loaders
Now that we've analyzed some of the modern conventions for using different files, let's look at some of the third party packages that support file loading.
- dotenv: Simple default .env file loading support.
- dotenv-flow: Adds full support for .env.development, .env.production, etc. In addition to a variable flow resolution process.
- dotenvx: Uses a secured encryption approach but essentially provides similar features to dotenv.
Loading config files with dotenv
If you're just looking for a simple environment loader, then dotenv is a good option.
To intall dotenv package, use:
npm install dotenv
Loading variable in modules
Simply place this code at the start of any module.
app.cjs
require('dotenv').congif()
The default config, will only load the .env
file regardless of the NODE_ENV
value.
To specify another file use the path config value to load a specific file if it exists.
app.cjs
require('dotenv').congif({path: '.env.production'})
Or you can use an array to find files in a sequential order, which will load and use the first file it finds.
app.cjs
require('dotenv').congif({path: ['.env.development.local', '.env.local', '.env.development', '.env']})
Setting the path config to an array of environment config files does not mean that the file has to exist, it will simply go through the list until it finds an available match.
Loading from scripts and CLI
The dotenv package also provides a way to preload variables from NPM scripts or command line using a flag
and path
conifg:
- -r: Stands for required and loads dotenv.
- dotenv_config_[path]: Sets the path option on dotenv config object.
For example:
{
"scripts": {
"dev": "NODE_ENV=development node -r dotenv/config app.cjs dotenv_config_path=.env.development",
"start:app": "NODE_ENV=production node -r dotenv/config app.cjs dotenv_config_path=.env.production"
},
}
This is usually the better option as it preloads variables and removes the environment config loading from inside your modules.
This resembles the direction that Node.js is heading with the --env-file
concept.
Loading with dotenv-flow
If you're building an app where you want the full environment flow loading process, then dotenv-flow is a package that can handle multiple config files.
The dotenv-flow is another popular solution, which actually uses a complete flow lookup process to find files in a certain sequential order.
To intall dotenv-flow package, use:
npm install dotenv-flow
The dotenv-flow is very similar to dotenv but with some added bonuses:
- It automatically adds a flow sequence to handle multiple environments based on the
NODE_ENV
value.
- It will use the first variable it finds as it goes through the sequence flow, allowing files earlier in the sequence to override variables later in the sequence.
dotenv-flow loading order
Here's the default flow order dotenv-flow uses to find variables.
- process.env (command line)
- .env.[NODE_ENV].local
- .env.[NODE_ENV]
- .env.local
- .env
It conveniently loads local files first, such as .env.local
and .env.*.local
which can be used to override base config files.
For example, any defaults set in .env.development
can be overriden using .env.development.local
.
Loading dotenv-flow in modules
Add the dotenv-flow to the top of your modules.
Using the default empty config automatically creates the flow order.
app.js
require('dotenv-flow').congif()
Or, using ESM module syntax.
import dotenvFlow from 'dotenv-flow';
dotenvFlow.config();
You can customize the flow using an array.
app.js
require('dotenv-flow').congif({path: ['.env.local', '.env.production', '.env']})
The dotenv-flow provides a big advantage for larger projects that might need finer grained control for local development.
Loading from scripts and CLI
The configuration is very similar to dotenv and a little less verbose.
$ NODE_ENV=production node -r dotenv-flow/config app.js
Or, using NPM scripts,
{
"scripts": {
"start": "NODE_ENV=production node -r dotenv-flow/config app.js"
}
}
A dotenv-flow example
Here's an example to show how dotenv-flow uses each config file to resolve and set the process.env
object.
I'll start up a node app in development, production and test, to show what variables are loaded for each environment.
dotenv-flow sample config files
The project should have each type of file in the root of the project.
sample dotenv-flow project
└── <project-root>
├── app.js
├── .env
├── .env.local
├── .env.development.local
├── .env.development
├── .env.test
├── .env.production.local
├── .env.production
└── package.json
To make things easier to visually scan and see what's actually being loaded, I'll only use the name of the environment as the value.
.env
DB_HOST=env
DB_PORT=env
DB_USER=env
DB_PASS=env
DB_NAME=env
.env.development.local
DB_NAME=env.development.local
.env.local
DB_USER=env.local
DB_PASS=env.local
.env.test
DB_NAME=env.test
.env.development
DB_NAME=env.development
.env.production.local
DB_USER=env.production.local
DB_PASS=env.production.local
DB_NAME=env.production.local
.env.production
DB_HOST=env.production
DB_PORT=env.production
DB_USER=env.production
DB_PASS=env.production
DB_NAME=env.production
Starting the app with dotenv-flow
Now, let's run the app in all three environments using the following scripts.
package.json
{
"scripts": {
"dev": "NODE_ENV=development node -r dotenv-flow/config app.js",
"start": "NODE_ENV=production node -r dotenv-flow/config app.js",
"test": "jest --setupFiles dotenv-flow/config"
}
}
Starting dotenv-flow in development
npm run dev
The environment values set for process.env
in development would be:
DB_HOST: 'env'
DB_PORT: 'env'
DB_USER: 'env.local'
DB_PASS: 'env.local'
DB_NAME: 'env.development.local'
Starting dotenv-flow in production
Now let's run the app in production
and it will show the following.
npm run start
The .env.production.local
can override and set local credentials to access the production resources like database.
DB_HOST: 'env.production'
DB_PORT: 'env.production'
DB_USER: 'env.production.local'
DB_PASS: 'env.production.local'
DB_NAME: 'env.production.local'
Starting dotenv-flow in test
Lastly, let's run dotenv-flow using the test environment.
If you noticed, I used the --setupFiles
flag to config with dotenv-flow to resolve the environment values:
jest with dotenv-flow
{
"test": "jest --setupFiles dotenv-flow/config",
}
You could use the same approach with dotenv config setup for test environments.
jest with dontenv
{
"test": "jest --setupFiles dotenv/config",
}
Now, the app can be tested using.
npm run test
We should the see the following values in the process.env
object.
DB_HOST: 'env',
DB_PORT: 'env',
DB_USER: 'env',
DB_PASS: 'env',
DB_NAME: 'env.test'
One thing to note that when running test, it will not load the .env.local
. The values will be resolved from the .env
base values and .env.test
overrides.
In Conclusion
So hopefully that was quick recap on how Node.js can support environment config files with multiple scopes.
These are just a few of the popular packages available to use. To me it's less about which package you choose and more about having a strategy to use with Node.js.
Here's a few key takeaways:
- There's no need to use every file in this example, it's just to show what's possible but use which files make sense for your project.
- Try to keep the
.env
loading out of your modules and use command line or script preloading if possible.
- Use the built-in
--env-file
as an option if you're using Node.js >=20.
- Try to keep
.env
files easy to manage and not bulky and complex.
Hope this article has helped.