12-factor Node.js application configuration management without the `config` npm package

Hugo Di Francesco - Feb 13 '19 - - Dev Community

The config npm package is great (npmjs.com/package/config), but it encourages confusing and non-12-factor-app-compliant patterns.

We’ll look at some of the patterns it encourages and why they’ll bring you struggles down the road as well a simple, single-file, no-dependency way to define your configuration.

Sprawling configuration: hard to pinpoint where config is set

The main thing it encourages is configuration sprawl: some of your configuration lives in JSON files, some of your configuration comes from environment variables (and is glued together using JSON files). Some config fields change depending on NODE_ENV, others do not.

Worst of all, config is dynamically loaded using a config.get('path.in.the.json.config.object') call. This creates an affordance for users to have deeply-nested configuration object(s), which isn’t desirable, your configuration should be minimal and it shouldn’t live in application code.

See the following from the “config” section in “The Twelve-Factor App” (see it in full at 12factor.net/config):

Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.

Non-granular configuration

Here’s another reason why having a package that makes it easy to have config objects isn’t a good idea according to 12 Factor again (see the full config section at 12factor.net/config):

In a twelve-factor app, env vars are granular controls , each fully orthogonal to other env vars. They are never grouped together as “environments” , but instead are independently managed for each deploy.

Having a default.json, production.json, test.json, custom-environment-variables.json is just not 12-factor, since you’re not supposed to group your config. It should be “here’s a database connection URL”, “here’s a backing service URL”, “here’s a cache connection string”.

It entices developers down the line keep adding switches and settings in a "database": {} field. Those concerns will not be orthogonal to each other, what’s more, they’re likely to be application-level concerns, eg. “should the database client try to reconnect?”. That’s not something you should be overriding with environment variables or toggling across the environments. It’s a setting that should be hard-coded into the application depending on whether or not the database is critical for example.

A single config.js file

config.js at the root of you application would look like this:

module.exports = {
  NAME_OF_CONFIG: process.env.NAME_OF_CONFIG || 'default-config',
  DATABASE_URL: process.env.DATABASE_URL,
  REDIS_URL: process.env.REDIS_URL || 'localhost:6379',
  X_ENABLED: process.env.X_ENABLED === 'true',
};

In the above, there are examples for how you would default a configuration variable (NAME_OF_CONFIG, REDIS_URL) and how you would check a boolean flag (X_ENABLED).

Making process.env fit for purpose

In Node.js process.env variables (environment variables) are strings, JavaScript is pretty loose with types, but it’s sometimes useful to convert process.env variables to another type.

Parsing a number from process.env

const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT, 10)
module.exports = {
  SESSION_TIMEOUT
};

Converting to Boolean from process.env

Comparing against the 'true' string tends to be enough:

module.exports = {
  IS_DEV: process.env.IS_DEV === 'true',
};

Consuming config.js

To get data from config.js would be like the following, where we conditionally set some 'json spaces', a request timeout and listen on a port with an Express app,

const { REQUEST_TIMEOUT, X_ENABLED, PORT } = require('./config')
const express = require('express')
const app = express()

if(X_ENABLED) {
  app.set('json spaces', 2)
}

app.use((req, res, next) => {
  req.setTimeout(REQUEST_TIMEOUT); 
  next()
})

app.listen(PORT, () => {
  console.log(`App listening on ${PORT}`);
});

Bonus: getting values from a .env file

Sometimes you’ll want to export values from a .env file into your shell sessionThe following snippet does just this and is an extract of this bash cheatsheet.

export $(cat .env | xargs)

Note the above works for *NIX environments

unsplash-logo
Filip Gielda

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .