async-config

2.0.0 • Public • Published

async-config

This module provides a simple asynchronous API for loading environment-specific config files and configuration data from other sources. This module utilizes the shortstop module to provide support for resolving values inside the configuration files based on user-provided "protocol handlers".

This module has extensive tests and is documented, stable and production-ready.

Table of Contents

Installation

npm install async-config --save

Overview

This module provides a simple API for loading environment-specific configuration files that is more flexible than alternatives. Its design was inspired by config and confit, but there are important differences. Unlike with the config module, this module does not use the exports of the module to maintain the loaded configuration. In addition, this module allows configuration files placed anywhere on disk to be loaded. Finally, this module provides an asynchronous API that allows configuration data to be loaded from remote sources and it avoids the use of synchronous I/O methods for reading files from disk. Compared to confit, this module is more flexible in how configuration files are named and located on disk.

Example

The basic usage is shown below:

Given the following directory structure:

.
└── config
    ├── config.json
    ├── config-production.json
    └── config-development.json

Each configuration file should contain valid JSON data such as the following:

{
    "foo": "bar",
    "complex": {
        "hello": "world"
    }
}

The following JavaScript code can be used to load the JSON configuration files and flatten them into a single configuration object:

const asyncConfig = require('async-config');
 
const config = await asyncConfig.load('config/config.json');
 
// The config is just a JavaScript object:
const foo = config.foo;
const hello = config.complex.hello;
 
// Use the get() method to safely access nested properties:
const missing = config.get('complex.invalid.hello');

Load Order

When loading a configuration file, the following is the default order that configuration data is loaded:

  1. path/{name}.json
  2. path/{name}-{environment}.json
  3. path/{name}-local.json
  4. NODE_CONFIG='{...}' environment variable
  5. --NODE_CONFIG='{...}' command-line arguments

For example, given the following input of "config/config.json" and a value of production, the configuration data will be loaded and merged in the following order:

  1. path/config.json
  2. path/config-production.json
  3. path/config-local.json
  4. NODE_CONFIG environment variable
  5. --NODE_CONFIG='{...}' command-line arguments

The load order can be modified using any of the following approaches:

const asyncConfig = require('async-config');
const config = await asyncConfig.load('config/config.json', {
    sources (sources) {
        // Add defaults to the beginning:
        sources.unshift('config/custom-detaults.json')
 
        // Add overrides to the end:
        sources.push('config/custom-overrides.json')
 
        // You can also push an object instead of a path to a configuration file:
        sources.push({ foo: 'bar' })
 
        // You can also push a function that will asynchronously load additional config data:
        sources.push(function () {
            return Promise.resolve({ hello: 'world' });
        });
    },
    defaults: ['config/more-detaults.json'],
    overrides: ['config/more-overrides.json']
});

Merging Configurations

A deep merge of objects is used to merge configuration objects. Properties in configuration objects loaded and merged later will overwrite properties of configuration objects loaded earlier. Only properties of complex objects that are not Array instances are merged.

Environment Variables

NODE_ENV

By default, the environment name will be based on the NODE_ENV environment variable. In addition, short environment names will be normalized such that prod becomes production and dev becomes development.

NODE_CONFIG

This environment variable allows you to override any configuration from the command line or shell environment. The NODE_CONFIG environment variable must be a JSON formatted string. Any configurations contained in this will override the configurations found and merged from the config files.

Example:

env NODE_CONFIG='{"foo":"bar"}' node server.js

Protocol Handlers

This module supports using protocols inside configuration files. For example, given the following JSON configuration file:

{
    "outputDir": "path:../build"
}

Protocol handlers can be registered as shown in the following sample code:

const path = require('path');
const asyncConfig = require('async-config');
 
const configDir = path.resolve(__dirname, 'config');
const configPath = path.join(configDir, 'config.json');
 
const config = await asyncConfig.load(configPath, {
    protocols: {
        path (path) {
            return path.resolve(configDir, path);
        }
    }
});

Since the path protocol handler is used, the final value of the "outputDir" directory will be resolved to the full system path relative to the directory containing the configuration file. For example:

{
    "outputDir": "/development/my-app/build"
}

By default, the following protocol handlers are registered:

import

Loads another configuration given by a path relative to the current directory.

For example:

{
    "raptor-optimizer": "import:./raptor-optimizer.json"
}

Since the async-config module is used to load the imported configuration file, the imported configuration file will also support environment-specific configuration files. For example:

  1. ./raptor-optimizer.json
  2. ./raptor-optimizer-production.json

path

Resolves a relative path to an absolute path based on the directory containing the configuration file.

require

Resolves a value to the exports of an installed Node.js module.

For example:

{
    "main": "require:./app-production"
}

Command Line Arguments

By default, this module will merge configuration data from the --NODE_CONFIG='{...}' argument, but you can also easily merge in your own parsed command line arguments. For example, this module can be combined with the raptor-args module as shown in the following sample code:

const asyncConfig = require('async-config');
const commandLineArgs = require('raptor-args').createParser({
        '--foo -f': 'boolean',
        '--bar -b': 'string'
    })
    .parse();
 
(async function () {
  const config = await asyncConfig.load('config/config.json', {
      overrides: [commandLineArgs]
  });
})();

Therefore, if your app is invoked using node myapp.js --foo -b hello, then the final configuration would be:

{
    "foo": true,
    "bar": "hello",
    ... // Other properties from the config.json files
}

App/Server Startup

It is common practice to load the configuration at startup and to delay listening on an HTTP port until the configuration is fully loaded. After the configuration has been loaded, the rest of the application should be able access the configuration synchronously. To support this pattern it is recommended to create a config.js module in your application as shown below:

config.js:

const asyncConfig = require('async-config');
 
let loadedConfig = null;
 
function configureApp (config) {
    // Apply the configuration to the application...
 
    // Make sure to invoke the promise when the application is fully configured
    return Promise.resolve();
}
 
exports.load = function () {
    // Initiate the loading of the config
    loadedConfig = await asyncConfig.load('config/config.json', {
        finalize: configureApp
    });
 
    return loadedConfig;
};
 
/**
 * Synchronous API to return the loaded configuration:
 */
exports.get = function() {
    if (!loadedConfig) {
        throw new Error('Configuration has not been fully loaded!');
    }
 
    return loadedConfig;
}

If you are building a server app, your server.js might look like the following:

const express = require('express');
const config = require('./config');
 
(async function () {
  // Asynchronously load environment-specific configuration data before starting the server
  const loadedConfig = await config.load();
 
  const app = express();
  const port = loadedConfig.port;
 
  // Configure the Express server app...
  app.listen(port, function() {
      console.log('Listening on port', port);
  });
})();

For a working sample server application that utilizes this module, please see the source code for the raptor-samples/weather app.

API

load(path[, options]) : Promise

The load() method is used to initiate the asynchronous loading of a configuration. The following method signatures are supported:

load(path) : Promise
load(path, options) : Promise

The path should be a file system path to a configuration file. If the path does not have an extension then the .json file extension is assumed. The input path will be used to build the search path.

The options argument supports the following properties:

  • defaults: An array of sources that will be prepended to the load order. Each source can be either a String file path, an async Function or an Object.
  • environment: The value of the environment variable (defaults to process.env.NODE_ENV or development).
  • excludes: An Array of sources to exclude. Possible values are the following:
    • "command-line" - ignore the --NODE_CONFIG command line argument
    • "env" - ignore the NODE_CONFIG environment variable
    • "env-file" - ignore path/{name}-{environment}.json
    • "local-file" - ignore path/{name}-local.json
  • finalize: An asynchronous Function with signature function (config) that can be used to post-process the final configuration object and possibly return an entirely new configuration object.
  • helpersEnabled: If set to false then no helpers will be added to the configuration object (currently the get() method is the only helper added to the final configuration object). The default value is true.
  • overrides: An array of sources that will be appended to the load order. Each source can be either a String file path, an async Function or an Object.
  • protocols: An object where each name is the protocol name and the value is a resolver function (see the shortstop docs for more details).
  • sources: A function that can be used to modify the default load order.

Notes

  • Loaded config objects are not cached by this module.

TODO

  • Add support for YAML and other configuration file formats?

Maintainers

Contribute

Pull requests, bug reports and feature requests welcome. To run tests:

npm install
npm test

License

ISC

Package Sidebar

Install

npm i async-config

Weekly Downloads

431

Version

2.0.0

License

ISC

Unpacked Size

31 kB

Total Files

19

Last publish

Collaborators

  • austinkelleher
  • dylanpiercey
  • mlrawlings
  • pnidem