Oleg Romanovskyi

The experienced  Software architect NodeJS developer AngularJS developer

Config files for nodeJS applications done right

Any application you write requires a configuration storage. And nodeJS applications are not an exception. Most typical config storage for node apps is a config file. It seems pretty simple at first glance but a lot of details and edge cases.

Let's check often used config approaches with their advantages and disadvantages.

Single config.js file to rule them all

Create a config.js file and add it to git repo. It's a first step in the direction of separated configuration. Most obvious step to take, when you had no config before. It's much better then connection strings and port numbers spread all over the code. But this approach has some serious issues.

Merge issues on code deployment.

In most cases server config differs from developers config. Every time you add a new config parameter - you have a chance to get a merge conflict on deployment.

Human Factor

When a project is big enough to have more than one person working on it, we have the second issue. In most companies I had experience with, developers environment is not identical. So each developer will modify config to get it working on his computer. And there is a chance to commit it accidentally. Such accidental commit will cause merge conflicts to all other developers. Moreover, it will cause merge conflict on a server as well.

Config file per environment

With this approach, we have at least 2 or 3 config files. Something like config.dev.js, config.prod.js, etc... This approach aims to reduce a chance of merge conflict on a server. And still gives means to add new parameters to server config by committing modified version. An environment is usually set by NODE_ENV environment variable.

In a typical case, having server config in source control is a major security flaw. Because you store real production config in VCS, accessible by all developers.

And config.dev.js still can be broken by accidental commits, as described in "Human Factor" section.

It's a viable solution for a small team of fully trusted developers.

Commit just config.example.js

You can commit an example of config and configure VCS to ignore "config.js" file. Then you create an actual config on every developer's workplace and server. It's secure because you don't store server config settings in VCS. It prevents accidental commits of developer's config. And it will not cause merge conflicts on a server.

Sounds good. But it still has a downside, which is large enough in my opinion. You need someone to modify config on servers every time you need to add a new config parameter even when default values are good enough. It breaks the idea of continuous delivery.

You can have default values in sources instead of default config. And then this approach will work. But I don't like default values spread all over the code. A single place to check what values are used sounds more convenient to me.

Hierarchical configuration (nconf)

I use nconf npm module. But key points are not limited to this particular module or even js.

The basic idea is a config inheritance. You have a basic config with default values. And you have different ways to override values. In most cases it's just a separate file. For example, I like settings.default.json in VCS and settings.json file with environment specific settings. settings.json is ignored by source control.

You have all benefits of config.example.js approach without its downsides.

Config merging by example

When I first used hierarchical configuration - I was confused by understanding how it decides what value to use. Let's say we have two files: settings.default.json

{
"mysql": {
"host": "localhost",
"port": 3306,
"login": "root",
"password": ""
}
}

and settings.json

{
"mysql": {
"login": "productionUser",
"password": "productionPassword"
}
}

Obviously nconf.get('mysql:login') will return "productionUser". But what will be returned by nconf.get('mysql')? Will it be an object with just "login" and "password" or an object from the default config with an overridden login and password?

Nconf will return a merged version

{
"mysql": {
"host": "localhost",
"port": 3306,
"login": "productionUser",
"password": "productionPassword"
}
}

So don't worry, you should not copy/paste the whole branch of config to modify a single value. It allows keeping a file with overridden values pretty compact. So you can easily see what was changed instead of picking from a whole set of values.

More ways to modify config, or "we need to go deeper."

With nconf, you can have more than two levels of inheritance. And not just files. I like to add environment variables as high priority storage. As a result, I always have a way to override values without touching file system at all.

Another way to add even more flexibility - is environment variable with a file name of additional config. I used this couple times. But it's almost too flexible. I don't like "magic" hard to track things in software development. And it not always easy to notice that someone broke something by injecting of additional config. If you need this - at least add some verbose logging with a list of used sources of config data.

Summary

There is no perfect way of config management. You can use any way that works for you. But it's important to understand what you get and what you loose. Hope this post will help you get a better understanding of different approaches and create more maintainable projects.