Trendy Coder Logo
  • Home
  • About
  • Blog
  • Newsletter

Mastering Node.js Env Variables in Minutes

Posted on: September 01 2025
By Dave Becker
Hero image for Mastering Node.js Env Variables in Minutes

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.

FileCommitted?Recommended Use
.env✅ YesDefault values common to all environments.
.env.local❌ NoSecrets and local-only overrides. Do NOT commit.
.env.development✅ YesDevelopment-specific config shared across team.
.env.development.local❌ NoDeveloper’s local overrides during development.
.env.production✅ YesProduction defaults (safe to commit).
.env.production.local❌ NoProduction-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:

  • .env
  • .env.development

❌ 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:

  • .env.production.local

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.

  1. process.env (command line)
  2. .env.[NODE_ENV].local
  3. .env.[NODE_ENV]
  4. .env.local
  5. .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.

Topics

SEOLinuxSecuritySSHEmail MarketingMore posts...

Related Posts

Hero image for Building React Components with Variants Using Tailwind CSS
Posted on: September 05 2025
By
Building React Components with Variants Using Tailwind CSS
Hero image for Next.js Layouts: Maximum Flexibility
Posted on: September 05 2025
By
Next.js Layouts: Maximum Flexibility
Hero image for Server-Side Pagination Made Easy in Payload CMS
Posted on: September 05 2025
By
Server-Side Pagination Made Easy in Payload CMS
Hero image for Payload CMS: Getting Started Using the New Join Field
Posted on: September 05 2025
By
Payload CMS: Getting Started Using the New Join Field
Hero image for How To Customize Flowbite React Components
Posted on: September 05 2025
By
How To Customize Flowbite React Components
Hero image for How Does Tailwind CSS Support Theming?
Posted on: September 05 2025
By
How Does Tailwind CSS Support Theming?
Hero image for Document Nesting With Payload's Nested Docs Plugin
Posted on: September 05 2025
By
Document Nesting With Payload's Nested Docs Plugin
Hero image for How to Build an Opt-In Page Using Tailwind CSS and Astro
Posted on: September 05 2025
By
How to Build an Opt-In Page Using Tailwind CSS and Astro
Trendy Coder Logo
Resources
  • Blog
Website
  • Home
  • About us
Subscribe

Get the latest news and articles to your inbox periodically.

We respect your email privacy

© TrendyCoder.com. All rights reserved.