Using Environment Variables in Node.js for App Configuration and Secrets
This article originally appeared on doppler.com and was written by Ryan Blunden, Developer Advocate at Doppler.
Many developers start with hard-coded config and secrets in either JSON or JavaScript files, and while this works, it poses a security risk as any developer with code access can potentially view secrets such as production database credentials and API keys.
If you're currently configuring your applications like this, don't worry, you're not alone! Most developers eventually realize that hard-coding credentials isn't the way to go, and begin looking for a more secure alternative in the form of a secrets manager.
This is exactly we created Doppler—giving developers an easy to use secrets manager for Node.js applications that centralizes secrets storage with an easy to use CLI for injecting secrets as environment variables.
Environment variables are considered the best way to configure applications, with the main benefits being:
- Secrets such as database credentials are not leaked into source code
- Ability to deploy an application in any environment without code changes
Using environment variables for configuring Node.js (and even JavaScript front-end) apps enables your application to be built and deployed anywhere, whether it's for local development on macOS, a container in a Kubernetes Pod, or modern Node.js hosting environments such as Vercel, Heroku, Netlify, Cloudflare Workers, and AWS Lambda.
Specifically, you could use environment variables to:
- Set the NODE_ENV environment variable to "development", enabling debug mode in your development environment
- Provide environment specific database credentials
- Supply an API key, e.g. STRIPE_API_KEY
- Set the HOSTNAME and PORT (e.g Heroku dynamically sets the PORT environment variable)
Note: If you haven't worked much with environment variables on the commandline, check out our ultimate guide for using environment variables in Linux and Mac.
Now that you know why environment variables are the best way to configure Node.js and JavaScript apps, let's explore the syntax for getting, setting, and loading environment variables.
How environment variables in Node.js are populated
When your Node.js application starts or a script is run, a new (child) process is created which inherits the environment variables from the parent process. Node parses the environment variables, creating a process.env object where every key and value is a string.
Using an interactive Node terminal, we can inspect the contents of process.env:
Now you know how environment variables in Node.js are populated, let's learn how to work with them.
How to read a Node.js environment variable
Accessing an environment variable from process.env is no different to that of a regular object:
const PATH = process.env.PATH
The one notable difference with the process.env object, is that every key and value will always be a string. This is because environment variables themselves can only ever be strings.
process.env.MY_INT = 1000;
console.log(typeof(process.env.MY_INT))
// >> 'string'
If a value you're accessing from an environment variable needs to be a non-string type, you must parse or cast it yourself, e.g. casting a PORT environment variable to an integer:
const PORT = parseInt(process.env.PORT);
While the syntax for accessing an environment variable is simple, more thought is required when deciding how environment variables are parsed and used, e.g. how to handle the case when a required variable is not supplied.
Required environment variable with no default
Because JavaScript won't error when accessing a key that doesn't exist in process.env , you'll need to design a solution for how your application will behave when a required environment variable is not supplied.
One option is to use the ok method from the assert module:
const assert = require('assert').strict
const API_KEY = process.env.API_KEY
assert.ok(API_KEY, 'The "API_KEY" environment variable is required')
If executed without setting the API_KEY environment variable, you'll be greeted with something like the following:
node:assert:399 throw err; ^ AssertionError [ERR_ASSERTION]: The "API_KEY" environment variable is required at Object. (/Users/rb/dev/required-api-key.js:3:8) at Module._compile (node:internal/modules/cjs/loader:1108:14) at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10) at Module.load (node:internal/modules/cjs/loader:973:32) at Function.Module._load (node:internal/modules/cjs/loader:813:14) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12) at node:internal/main/run_main_module:17:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: true, operator: '=='
}
Why use assert.ok? Because we want our application to throw an exception and exit if it's not configured properly.
You might be thinking "that's kinda ugly" which personally I'm ok with, as it should be a rare exception that your app is incorrectly configured.
If you wanted to handle this case with nicer output, you could use something similar to:
const API_KEY = process.env.API_KEY
if (!API_KEY) {
console.error('[error]: The "API_KEY" environment variable is required')
process.exit(1)
}
Now if executed without setting the API_KEY environment variable, the output would be:
[error]: The "API_KEY" environment variable is required
Whichever you choose is personal preference—the main thing being that you're handling the case of a missing required environment variable strictly and correctly.
Required environment variable with a default value
You can have a default value returned if an environment variable doesn't exist by using the || (OR) operator:
const HOSTNAME = process.env.HOSTNAME || 'localhost'
const PORT = process.env.PORT || 5000
So while the code to provide default values is simple, you should try to avoid defaults in your application code , the reasons of which I'll cover next.
Why to avoid default values for environment variables
The reason for avoiding defaults for environment variables is simple: A single source of truth should exist for any app config and secret value.
When debugging your application because it is misconfigured (and trust me, it's only a matter of time), the removal of defaults means every config or secret value was supplied by environment variables—that's your source of truth.
Default values in application code can make debugging a misconfigured application more difficult, as the final config values will likely be a combination of hard-coded default values and environment variables.
Make life easier for your future self (and team) by requiring all app config and secret values be set explicitly using environment variables.
How to set environment variables in Node.js
Setting or creating a new environment variable in Node.js is the same as setting a key on a standard object, except Node will implicitly convert any non-string value to a string.
It's recommended to always convert a value you wish to set as an environment variable to a string first, as future versions of Node may throw an exception if a non-string value is assigned.
process.env.FASTEST_SHIP = 'Millennium Falcon'
process.env.HYPERDRIVE_ACTIVATED = true // This may fail in future so don't do this
process.env.HYPERDRIVE_ACTIVATED = 'true' // Only assign string values
console.log(process.env.HYPERDRIVE_ACTIVATED)
// >> string
Changing environment variables in Node.js
When it comes to changing Node.js environment variables, there are two things to be aware of:
- Changes to environment variables in the parent process after the Node.js child process is created are not reflected in the Node.js process
- Changes to environment variables in the Node.js process do not affect the parent process
How to delete a Node.js environment variable
Deleting a Node.js environment variable is the same as deleting a key from a standard object:
Deleting an environment variable only affects the current script or application and doesn't affect the parent process.
As an aside, if you're confused as to why deleting a key from process.env is always true (even once the key has been deleted), you're not alone.
It's because it will only return false if the property is "non-configurable" , such as Math.PI or a property created as a result of calling Object.defineProperty() or Object.freeze().
Check out the Mozilla docs for the delete operator if you'd like to know more.
Why to avoid using a .env file for Node.js environment variables
As an application grows in size and complexity, so does the number of environment variables needed for app config and secrets.
A popular but problematic and insecure solution is to store the list of environment variables in a .env file and use the npm dotenv package to parse the .env file and populate the process.env object.
While .env files are simple and easy to get started with, they also cause a new set of problems such as:
- Running the risk of accidentally committing the .env file to source control
- Accidentally publicly exposing the .env file
- Keeping .env files in-sync across every local development environment
- No standardized approach to maintaining environment-specific .env files
- Onboarding a developer by sharing an unencrypted .env file with potentially sensitive data via Slack or other public messaging app could pose additional security issues
These are just some of the reasons why we recommend moving away from .env files and using an environment variable manager such as Doppler instead.
Doppler provides an access-controlled dashboard for managing environment variables in every environment with an easy-to-use CLI for accessing config and secrets that work for every language, framework, and platform.
You can check out our Mandalorion GIF sample application and deploy it to Vercel to see how to use Doppler for managing environment variables for a Node.js application.
What is the NODE_ENV environment variable used for?
The NODE_ENV environment variable originally came the Express web framework and was used to alter internal behavior, such as caching templates and using less verbose logs if NODE_ENV was set to production.
It has since become a popular Node.js convention for conditional logic based on the environment type, for example:
const isDevelopment = (process.env.NODE_ENV === 'development' ? true : false)
if(isDevelopment) {
// use verbose logging
// use server in development mode
}
Summary
Awesome work! Now you know how to use environment variables in Node.js for application config and secrets.
Although we're a bit biased, we encourage you to try using Doppler for managing your Node.js environment variables, and it's free to get started with our Community plan (unlimited projects, secrets, and users).
To see Doppler in action, check out the Mandalorion GIF sample application.
I hope you enjoyed the post and if you have any questions or feedback, you can send me an email at ryan[at]doppler[dot]com, or chat with the Doppler team in our Community forum.