Maintain Environment Configuration Settings and Secrets in Node.js Apps

It worth it to invest some time reaching a good solution for managing environment configuration settings and secrets for your apps to support multiple runtime environments (development, production, staging, etc) and multiple developers. These settings will likely require different configurations, depending on environment and/or developer, for things such as database connection information, log settings, API keys, usernames, passwords, etc. Additionally, we need to keep some of this information from being included in the codebase or repository but still be easily maintained. Let’s look at 2 packages that when used together create a nice solution to this problem.

Let’s begin by installing both packages packages:

  
    $ npm i config dotenv-safe
  

By default, config will look for a config folder at the root of your project for the configuration files, so let’s go ahead and create the folder with a default configuration file:

  
    $ mkdir config && touch ./config/default.json
  

As you can tell by the name of the file we just created, config settings will be saved as a JSON object. Our default.json file that contains settings for a database connection may look something like this:

  
    {
      "Database": {
        "host": "dbserver_dev.example.com",
        "db_name": "sample_db",
        "username": "user",
        "password": "secret_password"
      }
    }
  

Now, anywhere in the project where we need access to this information, we can required the config package:

  
    const config = require('config');
  

and then access the settings:

  
    config.get('Database.host'); // dbserver_dev.example.com
  

Pretty cool, right? The ability to keep these types of settings together is really handy. But the whole point of config is to help us manage these configurations across multiple runtime environments. To do this, we need to create a new configuration file for each environment. For example, to add our configuration for the production environment we need to create a production.json in the config folder. The production.json file may look something like this:

  
    {
      "Database": {
        "host": "dbserver_production.example.com"
      }
    }
  

Notice we don’t have to add the entire configuration to the new file. Anything that may stay the same as the default configuration, such as the database name, username, and password in our example, do not need to be in the production.json file. When the application is run in the production environment, config.get('Database.host') will be pulled from the production.json file and revert to the default.json configuration for everything else. To support any other runtime environment that requires different configuration we just need to create a new file - staging.json, qa.json, etc. and add in the new configuration settings.

But we have a glaring problem which you may have already noticed. Our database password is in the configuration. We generally want to keep that out of the codebase and repository for security reasons, so we don’t want to include it in config. For that we’ll use dotenv-safe.

The dotenv-safe package allows us to define any environment variables we want to keep private. It’s worth noting that we are now talking about the environment used by the Node process, not the runtime environment such as development, production, staging, etc. that we refer to when discussing configuration settings with config. By default dotenv-safe will pull values from a .env file located at the root of the project:

  
    $ touch .env
  

In our example, we want to keep our database password out of the codebase, so the .env file will look something like this:

  
    DB_PASSWORD=secret_password
  

Then, as early as possible in our project code we need load these environment variables:

  
    require('dotenv-safe').config();
  

Now any variable we define in the .env file will be available in our code as process.env.VARIABLE_NAME.

  
    db_password = process.env.DB_PASSWORD // = secret_password
  

Since we want to keep the information in this file private and out of the code repository, be sure to add .env to your .gitignore file.

The nice thing about dotenv-safe over the package it’s built upon, dotenv, is that we can define an example file without values that should to be added to the repository so it’s easy to keep track of what environment variables are expected without having to go through the code.

  
    $ touch .env.example
  
  
    DB_PASSWORD=
  

Not only can developers use this file to be aware of any environment variables they need to define in their respective .env file, but by default dotenv-safe will halt the execution of your program if any variable defined in .env.example is not found in .env.

With this current setup we can access any runtime environment setting using config.get('whatever.setting') and any private environment variable via process.env.VARIABLE_NAME. This works, but we can go a step farther so the only methods we need to use in our code are those offered by the config package, as well as take advantage of the hierarchical nature of config. We do this by telling config what environment variables to expect, and these values will override anything we have defined in the .json configuration files. To do this, we make a new file in the config folder called custom-environment-variables.json which may look something like this:

  
    {
      "Database": {
        "password": "DB_PASSWORD"
      }
    }
  

Now, when we call config.get('Database.password'), config will look for an environment variable called “DB_PASSWORD”. If it’s not found it will use the value found in the .json file that matches our current runtime environment, and if it’s not found there it will load from default.json.

NOTE: Since we are now keeping the DB_PASSWORD value in .env file, be sure to remove it from the original default.json file we defined at the beginning of this tutorial.

So there you have it - config and dotenv-safe make for a nice setup to manage runtime environment configurations and environment variables!