Managing Multi-Environment Configurations in Node.js with a Single Configuration File

posted on July 18, 2021

In the past, when I was working on Node.js projects that could run under different environment settings (e.g., local, develop, staging, production), a typical pattern was to create multiple configuration files per environment. For example, a project with three environments: local, develop, and production, would need three configuration files:

project_root
├── config.local.json
├── config.dev.json
└── config.prod.json

Each of these files defines an object with a similar structure but different configuration values for every environment. For example, the config.local.json might define the hosts for the local API server and the local database connection string:

{
    "apiHost": "https://localhost:8080",
    "mongoUri": "mongodb://localhost:27020/app",
    "logLevel": "debug"
}

Then, a config.js module could use a simple logic that loads the correct configuration file for the target environment, for example, by checking an environment variable such as NODE_ENV:

// config.js

import fs from 'fs';

function getConfigFileForEnvironment(env) {
    switch (env) {
        case 'local':
            return 'config.local.json';
        case 'development':
            return 'config.dev.json';
        case 'production':
            return 'config.prod.json';
    }
}

const configFilePath = getConfigFileForEnvironment(process.env.NODE_ENV);
const rawData = fs.readFileSync(configFilePath);
const config = JSON.parse(rawData);

export default config;

However, as the project and the team working on it grew, these configuration files often came out of sync. For example, while working on a new feature, a developer could add a new field to a local configuration and forget to add it to other configuration files and break other environments.

I've identified the following drawbacks when using this pattern:

  • No validation for missing keys between environments.
  • Risk of having out-of-sync configuration files.
  • No way to define default or fallback values.
  • Difficulty seeing all the configuration values per environment per key in one place.
  • Difficulty creating new environments

Therefore, I created a small utility that improves multi-environment configuration management by storing all configurations in a single JSON file to solve these problems.

Solution - single-config

Single-Config is a small npm package that solves the problems described above. It lets you define a single configuration file for all environments. It can also generate types for the configuration files using TypeScript.

Continuing the previous example, using single-config, you can define a single configuration file config.json:

{
    "_envs": ["local", "dev", "prod"],
    "apiHost": {
        "local": "https://localhost:8080",
        "dev":  "https://dev.api.example.com",
        "prod":  "https://api.example.com"
    },
    "mongoUri": {
        "local": "mongodb://localhost:27020/app",
        "dev": "mongodb://dev.mongo.example.com:27017/app",
        "prod": "mongodb://mongo.example.com:27017/app"
    },
    "logLevel": {
        "default": "info",
        "local": "debug"
    }
}

After installing single-config and running the buildconfig command:

npm i single-config -g
buildconfig --env=local

It will generate a config.js module exporting an object with all the properties resolved to the "local" environment:

// This file was automatically generated at 2021-07-18T15:46:04.304Z
module.exports = {
    "env": "local",
    "apiHost": "https://localhost:8080",
    "mongoUri": "mongodb://localhost:27020/app",
    "logLevel": "debug"
};

The following diagram depicts the build logic:

single-config Diagram