DEV Community

Cover image for Bulletproof Your Node.js Backend: Manage Environment Variables with Confidence
vibhanshu pandey
vibhanshu pandey

Posted on

Bulletproof Your Node.js Backend: Manage Environment Variables with Confidence

Pretext

Managing environment variables is a crucial aspect of building robust and secure Node.js applications. These variables contain sensitive information such as API keys, database credentials, and other configuration details that your application needs to function properly. However, managing environment variables can quickly become a headache, especially when dealing with multiple environments, team members, and deployment scenarios.

One of the most significant pain points of environment variable management is the lack of type safety. Without type safety, it's easy to introduce bugs and security vulnerabilities, as developers may accidentally use the wrong variable or misspell its name. Additionally, environment variables can be difficult to keep track of, as they're often scattered across different files and deployment environments.

Fortunately, there are solutions to these problems. In this blog post, we'll explore how to manage environment variables in a type-safe manner using TypeScript. We'll look at why type safety is essential, how it can help prevent bugs and security issues, and best practices for implementing it in your Node.js backend. By the end of this post, you'll have a better understanding of how to manage your environment variables with confidence and peace of mind.

The Problem

Using dotenv for environment variable management doesn't allow breaking up the environment variables into different files like .env, .env.development, and .env.production, among others. Additionally, it doesn't allow for variable expansion. For instance, when using dotenv, the following code will not work:

APP_NAME="My app name"
APP_DESCRIPTION="$APP_NAME is awesome"
Enter fullscreen mode Exit fullscreen mode

Now, I expect APP_DESCRIPTION to be "My app name is awesome" but it won't be because dotenv doesn't perform variable expansion by itself, instead I'd have to use another library dotenv-expand to achieve this.

Also, I like using typescript and using process.env doesn't really give me any clue about my environment variables.

This approach works but has following drawbacks:

  1. No IDE support for process.env.APP_NAME
  2. No auto suggestion for process.env.* variables.
  3. No Type checking is available for process.env.APP_NAME, and writing custom type is a hassle, because every time you need to add or remove an environment variables you'd have to update it's types as well.
  4. No validation is performed on the values of these environment variables.

Typically, you should structure your .env files like the following:

.env
.env.development
.env.test
.env.production

Optionally:

.env.development.local
.env.test.local

where .env contains those environment variables that are common across all environments like:

  1. APP_NAME
  2. PORT
  3. TZ etc.

And environment specific file like .env.development should contain environment specific variables like:

  1. NODE_ENV
  2. DATABASE_URL
  3. SECRET etc.

The Solution

Now, to eliminate all the drawback mentioned above, like type safety, variable expansion, and validation. Follow the solution below

Now obviously we'll still be using the file structure described above to keep our environment variables split among environment specific files.

This can be achieved with dotenv-cli, a new fast-growing npm package that can be used to eliminate the need of dotenv, dotenv-expand, and our custom code for loading multiple .env.* files.

Syntax

npm i -D dotenv-cli
Enter fullscreen mode Exit fullscreen mode

update your package.json file scripts to be:

"dev": "dotenv -c development -- <your command goes here ex: ts-node src/index.ts>",
"build": "tsc",
"start": "dotenv -c production -- node dist/index.js",
Enter fullscreen mode Exit fullscreen mode

What we're doing in dev script is using dotenv-cli with -c flag to load:

  1. .env
  2. .env.development
  3. .env.development.local (if present)

In start script we're loading:

  1. .env
  2. .env.production

TIP: Don't use .env.production.local file.

So, This solves our multi-environment multi-file problem, and reduced our dependency from 2 package to one, and eliminate any custom code writing for loading .env files.

Now, How can we solve our IDE support, Type checking, and validation problem.
For that we'll use envalid, a fast growing npm package to validate and provide types for our environment variables.

To get started:

npm i envalid
Enter fullscreen mode Exit fullscreen mode

And yes we need it as our production dependency.

Now, we'll create a file right beside our index.ts file named env.ts, with the following content:

import { cleanEnv, str, email, json, port } from 'envalid'

// INFO: using $env to differentiate it from process.env 
export const $env = cleanEnv(process.env, {
  PORT:               port(),
  API_KEY:            str(),
  ADMIN_EMAIL:        email({ default: 'admin@example.com' }),
  EMAIL_CONFIG_JSON:  json({ desc: 'Additional email parameters' }),
  NODE_ENV:           str({ choices: ['development', 'test', 'production', 'staging']}),
})

$env.isProduction    // true if NODE_ENV === 'production'
$env.isTest          // true if NODE_ENV === 'test'
$env.isDev           // true if NODE_ENV === 'development'
Enter fullscreen mode Exit fullscreen mode

And use $env instead of process.env.
By using $env we'll get:

  1. Auto-completion ex: $env. should list all possible values in your IDE.
  2. Type checking ex: $env.PORT will be of number type.
  3. IDE support like: Reference count, Jump to code, show definition etc.
  4. Validation provided by envalid, as well as custom validation logic that we can combine with envalid, to really fine tune according to our requirement.
  5. BONUS by using env.ts file we eliminate the requirement for a .env.example file that a lot of us use to keep track of all the environment variables names that is used all across the project.

And there you have it

To learn more visit:

  1. envalid
  2. dotenv-cli

Feel free to comment down below, and let me know what you think.

Top comments (2)

Collapse
 
forceunwrap profile image
Sascha Gordner

Thanks for the nice article.
Just wanted to mention that since Node v20.6.0 there is native support for loading env files, so that one does not have to use dotenv-cli anymore: nodejs.org/en/blog/release/v20.6.0

Collapse
 
koroglumert profile image
Mert İsmail Köroğlu

I tried to review the update but the update still doesn't allow me to do the following?

APP_NAME="My app name"
APP_DESCRIPTION="$APP_NAME is awesome"