Simplify Node Configuration With Object Assign

09-25-2018

I work on some sizable enterprise NodeJS applications and the thing that bites us most often are our configuration files. I like to use a simple configuration file to define static variables that are used throughout my apps to avoid hardcoded values whenever possible. This allows me to easily configure my apps across different environments, etc. But when it comes to managing the different versions of configuration files for all of this different possible scenarios it can become daunting and often error prone. Leading to hours wasted debugging. Here is a solution that uses Object.assign() to basically ‘merge’ configuration files together to help manage the files.

The idea is simple, we have a base config.js file that contains our ‘default’ values for the application. Then we have specific configuration files that either manage or override the ‘default’ values based on the configuration to be used. That is determined based on some condition, i.e. environment, in debug mode, etc. and passed in as an environment variable. The result of the ‘merge’ between the configuration files is what our app will use when running.

Let’s look at the folder structure of a example application. We have app.js as the entry point for the app and a folder called ‘conf’. Inside ‘conf’ are my various configuration files along with an index.js file.

.
+– app.js
+– conf
| +– config.js
| +– dev-config.js
| +– qa-config.js
| +– index.js

First, we have our app.js file as the entry point for the NodeJS app. Very simple, I’m just requiring the ‘conf’ folder and console.log()’ing the result. Anywhere within the application that I need access to the configuration file I include the ‘conf’ folder by requiring it in the same way.

app.js

1
2
3
4
5
'use strict';

const config = require('./conf');

console.log(config);

Our base configuration file, config.js will contain any values that are consistent across all of the various conditions and need to only be represented once. This can be things like static URLs, port numbers, connection limits, etc.

config.js

1
2
3
4
5
6
7
'use scrict';

module.exports = {
'dbPort': 3306,
'dbConnectionLimit': 400,
'serverPort': 3000
};

Then our specific configuration files might look something like this:

dev-config.js

1
2
3
4
5
6
7
'use scrict';

module.exports = {
'accessKey': 'gbj876ytgfhjknbvf56yuh',
'dbConnectionLimit': 100,
'environment': 'dev'
};

The combining of the various configuration files happens in the index.js file inside the ‘conf’ directory. Because we only require the ‘conf’ folder, const config = require('./conf'); Node will look for and execute a file called index.js during the bootup process. Within our index.js file is a simple switch statement that accepts the ‘NODE_ENV’ environment variable to determine what configuration(s) to merge and use. This is where the ‘magic’ of this pattern happens. We use Object.assign() to ‘merge’ the various configuration files together to create a single configuration file that is used throughout our application.

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const config = require('./config');
const dev = require('./dev-config');
const qa = require('./qa-config');

switch (process.env.NODE_ENV) {
case 'dev':
module.exports = Object.assign(config, dev)
break;
case 'qa':
module.exports = Object.assign(config, qa);
break;
default:
module.exports = config;
break;
}

Object.assign() accepts multiple objects as arguments and keys that are duplicated across the objects are ‘overwritten’ by subsequent objects with the same key. The result is a single configuration file based on the condition of my application. So if we startup our Node application with the following command NODE_ENV=dev node app.js, NodeJS will first call the index.js file inside the ‘conf’ directory to determine which configuration to use based on the NODE_ENV=dev value. In this example we would combine the config.js and dev-config.js files to get the following result:

1
2
3
4
5
6
7
{ 
dbPort: 3306,
dbConnectionLimit: 100,
serverPort: 3000,
accessKey: 'gbj876ytgfhjknbvf56yuh',
environment: 'dev'
}

By using this pattern we reduce the possibility of values not being updated across various configuration files that are consistent across the files.