Managing Your Distributed Node.js Application Environment and Configuration
Tags: javascript, node, programming, api
Published: 2022-10-19 20:23:49

_See how to effectively manage the environment and configuration of your distributed Node.js applications._

Node.js applications rely on [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to differentiate between…environments!

This means that applications running locally often behave differently to those being deployed to testing, staging, or production environments.  Often, this just means listening on a different port, or pointing to a different database url.  

[The Largest River](https://dev.to/bretthoyer/series/19070), my first foray into distributed application development, leverages both environment variables and configuration files to differentiate between the mode in which the code is being run, and the cloud region where it's being run.

In this article, I'm going to demonstrate how the [dotenv](https://www.npmjs.com/package/dotenv) and [node-config](https://www.npmjs.com/package/config) NPM packages can be used together to keep your Node.js application code organized across environments.

## Setting the Environment

Application configuration can be done in a variety of ways, depending on the complexity of the system being built. In The Largest River project, I've chosen to use environment variables minimally, only setting `NODE_APP_INSTANCE` and `NODE_ENV`.  

Let's see how this works.
```
#.env
NODE_APP_INSTANCE=los-angeles
```
```
# server.js

if (process.env.NODE_ENV === "development") { 
  require("dotenv").config();
}

// rest of file has access to process.env.NODE_APP_INSTANCE
if (process.env.NODE_APP_INSTANCE === “london”) {
  // connect to database node nearest to London
}
...
```
You might be wondering where I've set the value of `NODE_ENV`. Well, this variable is set to 'development' by default. When running our node script, the following two commands are equivalent.

```
NODE_ENV=development node server.js

node server.js
```

In production, we set the variables explicitly in the startup script.

```
# startup_script.sh
...
// setting NODE_APP_INSTANCE environment variable from instance metadata
NODE_APP_INSTANCE=$(curl http://metadata.google.internal/computeMetadata/v1/instance/attributes/instance_id -H "Metadata-Flavor: Google")
echo "NODE_APP_INSTANCE=${NODE_APP_INSTANCE}" | sudo tee -a /etc/environment

//setting NODE_ENV to production
echo "NODE_ENV=production" | sudo tee -a /etc/environment
source /etc/environment

//starting our node server with explicit command line arguments
NODE_ENV=$NODE_ENV NODE_APP_INSTANCE=$NODE_APP_INSTANCE node server.js
```

This ensures that even on server reboots, our application will start reliably, with the proper environment set. The `/etc/environments` file is system-wide and persistent, making it a reasonable place to store our environment variables on production servers.

## Beyond the Basics

With our environment settled, it's time to check out our application configuration.  

What if our app needs more expressive configuration? Environment variables _can_ do the job, but their values are always strings. What if we want to include objects and nested data types?  

[Some developers](https://12factor.net/config) would say, "[Stringify your JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) and add it to your environment."

While I understand the sentiment, I disagree. It is much more pleasant to work directly inside a JSON file. So long as you take care to not check sensitive information into source control, this can be a powerful tool in your development arsenal.

Here's an example, utilizing the node-config package:
```
# default.json
{
  "Databases": [
    {
      "id": "single_region",
      "type": "single_region",
      "host": "[HOST_FOR_DATABASE]", 
      "primaryRegion": "us-west2",
      "username": "[DB_USERNAME]",
      "password": "[DB_PASSWORD]",
      "cert": "[PATH_TO_DB_CERT]",
      "dev_cert": "[PATH_TO_CERT_IN_DEVELOPMENT]",
      "label": "Single-region, multi-zone",
      "sublabel": "3 nodes deployed in US West",
      "nodes": [
        {
          "coords": [35.37387, -119.01946],
          "label": "Bakersfield",
          "zone": "us-west2",
        },
        {
          "coords": [34.95313, -120.43586],
          "label": "Santa Maria",
          "zone": "us-west2",
        },
        {
          "coords": [32.71742, -117.16277],
          "label": "San Diego",
          "zone": "us-west2",
        }
      ]
    },
    ...
  ]
}
```
```
# server.js

const config = require("config");
const Databases = config.get("Databases");
```

By utilizing a JSON configuration file, we're able to easily edit our database details. Given that The Largest River features multiple [YugabyteDB Managed](https://www.yugabyte.com/managed/) databases with varying deployments, it is helpful to not be restricted to key-value string pairs.

As a matter of interest, this is what working with the same configuration would look like in an environment variable.
```
# .env
DATABASES='{"Databases":[{"id":"single_region","type":"single_region","host":"[HOST_FOR_DATABASE]","primaryRegion":"us-west2","username":"[DB_USERNAME]","password":"[DB_PASSWORD]","cert":"[PATH_TO_DB_CERT]","dev_cert":"[PATH_TO_CERT_IN_DEVELOPMENT]","label":"Single-region, multi-zone","sublabel":"3 nodes deployed in US West","nodes":[{"coords":[35.37387,-119.01946],"label":"Bakersfield","zone":"us-west2"},{"coords":[34.95313,-120.43586],"label":"Santa Maria","zone":"us-west2"},{"coords":[32.71742,-117.16277],"label":"San Diego","zone":"us-west2"}]}]}'
```
```
# server.js
if (process.env.NODE_ENV === "development") { 
  require("dotenv").config();
}

const Databases = JSON.parse(process.env.DATABASES)
```


Earlier we set an environment variable called `NODE_APP_INSTANCE`.  [This variable](https://github.com/node-config/node-config/wiki/Environment-Variables#node_app_instance) can be used to set instance-specific configuration in a multi-instance deployment. This can be hugely helpful if instances of the same application need to behave differently.  

The Largest River is comprised of six application instances, each connecting to different database nodes, depending on their configuration.

![The Largest River Application Instances](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iztkfngvtddkh6r88vex.png)
<figcaption>The Largest River Application Instances</figcaption>

If a `default.json` needs to be overridden, these application instances will read from `production-los-angeles.json`, `development-mumbai.json`, etc. depending on the environment and cloud region.

## Wrapping Up

The way you manage your application environment and configuration is entirely up to you. After all, you're the one that has to work with it!  

There are no hard and fast rules. Personally, I'm always looking for ways to remove configuration details from my application logic. This has a number of benefits. For example, as my applications evolve, there is a single source of truth, and reduced code duplication.  

I hope you'll consider some of these tips when you create your next distributed Node application. And remember, this is your journey, do what works best for _you_!

Look out for my next blog, which will look at [distributed SQL](https://www.yugabyte.com/tech/distributed-sql/) database configurations in depth!