mconf
v2.0.0
Published
Tiny environment-aware config loader for Node.js with deep-merge layering.
Maintainers
Readme
mconf
Tiny environment-aware config loader for Node.js. Pick an environment name from
process.env, layer the matching config file on top of a shared base, and get
back a single merged object. Zero runtime dependencies.
Install
npm install mconfRequires Node.js 20 or newer.
Usage
Create a directory of CommonJS config files (one per environment):
config/
├── production.js
├── rc.js
└── develop.jsEach file exports a plain object:
// config/production.js
module.exports = {
service: 'api',
port: 80,
feature: { flags: { darkMode: true } },
};ESM
import { Mconf } from 'mconf';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const config = new Mconf(join(here, 'config'), ['production', 'rc', 'develop']).getConfig();
export default config;CommonJS
const { Mconf } = require('mconf');
const path = require('node:path');
const config = new Mconf(path.join(__dirname, 'config'), ['production', 'develop']).getConfig();
module.exports = config;Run with the env you want:
NODE_ENV=production node app.jsHow layering works
mconf always merges in this order:
- The base layer (
productionby default). - The layer matching the environment selected from
process.env.
By default mconf is strict: if the requested env is not in availableEnvs,
getConfig() throws. Set strict: false to opt into the legacy silent
fallback to fallbackEnv (default develop).
The merged result is augmented with a reserved environment field naming the
layer that was applied last. Config files must not declare their own
environment key — the loader rejects collisions with a TypeError.
new Mconf(dir, ['production', 'develop'], {
baseEnv: 'production', // first layer applied; must be in availableEnvs
fallbackEnv: 'develop', // used only when strict is false; must be in availableEnvs
strict: true, // default; set false to silently fall back instead of throwing
envName: 'NODE_ENV', // env var read for the current environment
deepMerge: true, // false → top-level overwrite only
});API
new Mconf(configDir, availableEnvs, options?)
| Argument | Type | Notes |
| --------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| configDir | string | Absolute path to the directory holding the config files. |
| availableEnvs | string[] | Whitelist of permitted env names. Each entry must match ^[A-Za-z0-9][A-Za-z0-9_.-]*$ (alphanumeric first character, then alnum, _, -, .). |
| options | MconfOptions | See above. baseEnv and fallbackEnv must both be members of availableEnvs. |
Instance methods
getConfig()— load and merge the layers, return the merged object.setEnvName(name)— change the env var read atgetConfig()time. Returnsthis.setDeepMerge(boolean)— toggle deep merge. Returnsthis.getEnvironmentFromGlobalEnv()— read the raw value fromprocess.env.
Config file format
Config files are loaded synchronously via createRequire, so they must be
CommonJS. In an ESM project ("type": "module" in package.json), name them
.cjs or place them under a directory with a nested
{ "type": "commonjs" } package.json. Top-level-await ESM and dynamic
import() are intentional non-goals: a config loader that returns a Promise
would force every caller to be async.
If your build pipeline outputs Babel/TypeScript-style transpiled modules
(exports.__esModule = true; exports.default = …), mconf unwraps them
automatically. Plain CJS configs that legitimately export a default key are
left untouched.
Migration from 1.x
2.0.0 is a major bump. Highlights:
- Minimum Node.js raised to 20 LTS.
- Built and shipped as dual ESM + CJS with TypeScript types.
- The class is exposed as a named export only:
import { Mconf } from 'mconf'/const { Mconf } = require('mconf'). The legacyrequire('mconf').defaultshape is gone. - Strict by default.
getConfig()now throws when the requested env is not inavailableEnvs. Passstrict: falseto restore the silent fallback-to-developbehaviour from 1.x. baseEnvandfallbackEnvare validated againstavailableEnvsat construction time; previously-silent misconfigurations now throw immediately.- The
setEnv()alias was removed; usesetEnvName(). - Removed Babel toolchain and the
extenddependency. Deep-merge is implemented inline, rejects prototype-pollution keys (__proto__,constructor,prototype), and never mutates inputs (no shared references between calls). - Env names are validated; path traversal is no longer possible via
availableEnvsor craftedprocess.envvalues. - Errors thrown by the loader now carry an
Error.cause, and the wording for load failures has changed. If your code matched on the old strings, update. - Configs must export a plain object. Primitives now throw instead of silently leaking through.
- Configs must not declare an
environmentkey — that name is reserved for the merged result. - Configs are loaded synchronously via
createRequire(CommonJS only). - Repeated
getConfig()calls no longer accumulate state internally and no longer share nested-object references with previously-returned configs.
Development
npm install
npm run lint
npm test
npm run test:coverage
npm run buildSee CONTRIBUTING.md for details.
License
ISC © Stanislav Gumeniuk
