Managing Multi-Environment Configurations in Node.js with a Single Configuration File
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: