A powerful HOCON (Human-Optimized Config Object Notation) parser and loader for Node.js. We fully handle:
- Environment variable substitutions (
?ENV_VAR
/${?ENV_VAR}
) - Multiple-file includes (
include "overrides.conf"
) - Nested objects & arrays (dotted keys → nested objects)
- Key merging (last definition wins, partial array overrides, etc.)
- Programmatic overrides for advanced usage
- Built-in CLI + ENV merging in the
parse
function - Zero dependencies – only Node’s built-ins
- Jest-based tests ensuring quality
No need to be humble: hocon-config
is robust yet straightforward, making your Node.js configuration a breeze.
npm install hocon-config
Once installed, you can simply import or require it in your Node.js code.
-
Create a HOCON file (e.g.,
config/base.conf
):app.name = "ExampleApp" database { host = "localhost" port = 5432 }
-
Use the
parse
function to load it, automatically merging environment variables & CLI arguments:const path = require('path'); const { parse } = require('hocon-config'); // Suppose we want to read config/base.conf // We'll pass no options => parseEnv=true, parseArgs=true, envPrefix='' const filePath = path.join(__dirname, 'config', 'base.conf'); const config = parse(filePath); console.log(config); // Might print: // { // app: { name: 'ExampleApp' }, // database: { host: 'localhost', port: 5432 } // }
-
Done! You have a Node.js object ready to use, and if you define environment variables or pass CLI arguments (prefixed keys, etc.), they override the config automatically.
/**
* parse(filePath, [runtimeOptions]):
* 1) Gather overrides from process.env + process.argv
* 2) parseFile(...) with those overrides
*
* runtimeOptions:
* envPrefix?: string (default "")
* parseEnv?: boolean (default true)
* parseArgs?: boolean (default true)
* debug?: boolean
*/
function parse(filePath, runtimeOptions = {}) {
const {
envPrefix = "",
parseEnv = true,
parseArgs = true,
debug = false,
} = runtimeOptions;
// Collect env-based overrides (keys with prefix => dottedKey)
let envMap = {};
if (parseEnv) envMap = buildEnvMap(process.env, envPrefix);
// Collect CLI-based overrides (--app.name=Override)
let argMap = {};
if (parseArgs) {
const argv = process.argv.slice(2);
argMap = buildArgMap(argv);
}
// CLI overrides env if there's a conflict
const finalOverrides = { ...envMap, ...argMap };
// parseFile with final overrides
return parseFile(filePath, {
debug,
overrides: finalOverrides,
});
}
By default:
envPrefix=""
ensures only environment variables likeapp_name
→'app.name'
are considered.parseEnv=true
merges environment variables.parseArgs=true
merges CLI arguments of the form--some.dotted.key=value
.- The final config merges these on top of your HOCON file’s contents.
Below are 11 scenarios from simple key-values to complex multi-file merges, environment usage, CLI arguments with nested keys, etc.
config/s1.conf
:
hello = "world"
Code:
const conf = parse('config/s1.conf');
console.log(conf);
// => { hello: 'world' }
Nothing fancy.
config/s2.conf
:
app {
name = "Scenario2"
nested {
level = "deep"
}
}
Code:
const conf = parse('config/s2.conf');
console.log(conf);
// => {
// app: {
// name: 'Scenario2',
// nested: { level: 'deep' }
// }
// }
Objects like app.nested.level
become nested JS objects.
config/s3.conf
:
server.ports = [8080, 9090]
server.ports = [10000]
Code:
const conf = parse('config/s3.conf');
console.log(conf);
// => { server: { ports: [ '10000' ] } }
The second line overwrote the entire array—last definition wins.
config/s4.conf
:
feature.flag = false
feature.flag = ${?FEATURE_FLAG}
Code:
process.env.FEATURE_FLAG = 'true';
const conf = parse('config/s4.conf');
console.log(conf);
// => { feature: { flag: 'true' } }
config/s5-base.conf
:
server.ports = [8080, 9090, 10000]
config/s5-override.conf
:
include "s5-base.conf"
server.ports = [${?APP_PORT}]
Usage:
delete process.env.APP_PORT;
const conf1 = parse('config/s5-override.conf');
console.log(conf1.server.ports);
// => [ '8080', '9090', '10000' ] (unchanged)
process.env.APP_PORT = '9999';
const conf2 = parse('config/s5-override.conf');
console.log(conf2.server.ports);
// => [ '9999', '9090', '10000' ]
If $APP_PORT
isn’t defined, we skip overwriting the array.
config/s6.conf
:
app.name = "BaseCLI"
server.port = 3000
CLI:
node index.js --app.name=MyCLIoverride --server.port=9999
Code (index.js
):
const conf = parse('config/s6.conf');
// => merges env vars w/ prefix '' plus CLI
console.log(conf);
// => { app: { name: 'MyCLIoverride' }, server: { port: '9999' } }
parse
sees --app.name=MyCLIoverride
→ {'app.name': 'MyCLIoverride'}
, overshadowing file definitions.
config/s7-base.conf
:
app {
name = "Scenario7"
}
include "s7-mid.conf"
config/s7-mid.conf
:
app.midKey = true
include "s7-leaf.conf"
config/s7-leaf.conf
:
app.final = "leaf"
Code:
const conf = parse('config/s7-base.conf');
console.log(conf);
// => {
// app: {
// name: 'Scenario7',
// midKey: 'true',
// final: 'leaf'
// }
// }
All merges happen in correct order.
process.env.FEATURE_FLAG = 'false';
const conf = parse('config/s8.conf', {
// override everything, if we want
overrides: {
'database.host': 'prod-db.internal',
'app.enableBeta': true
}
});
console.log(conf);
// merges file + env + CLI + final overrides
Any overrides
object merges last, overshadowing everything else.
config/s9.conf
:
app.name = "BaseEnvCLI"
app.debug = false
CLI:
node index.js --app.debug=true
Code:
process.env.app_name = 'EnvOverride';
const conf = parse('config/s9.conf', {
overrides: { 'app.logLevel': 'VERBOSE' }
});
// Priority order (lowest -> highest):
// 1) file => app.name=BaseEnvCLI, app.debug=false
// 2) env => app.name=EnvOverride
// 3) CLI => app.debug=true
// 4) overrides => app.logLevel=VERBOSE
console.log(conf);
// => {
// app: {
// name: 'EnvOverride',
// debug: 'true',
// logLevel: 'VERBOSE'
// }
// }
const { parseString } = require('hocon-config');
const hoconData = `
server { port = 3000 }
feature.enabled = ${'?FEATURE_FLAG'}
`;
process.env.FEATURE_FLAG = 'true';
const inlineConfig = parseString(hoconData, __dirname, { debug: true });
console.log(inlineConfig);
// => { server: { port: '3000' }, feature: { enabled: 'true' } }
No file needed, just inline usage.
config/s11.conf
:
app {
nestedKey = "original"
}
CLI:
node index.js --app.nestedKey=CLIOverride
Code:
// parse() sees envPrefix '' for env, and parseArgs for CLI
const conf = parse('config/s11.conf');
console.log(conf);
// => { app: { nestedKey: 'CLIOverride' } }
parse
automatically reads process.argv
→ --app.nestedKey=CLIOverride
=> {'app.nestedKey': 'CLIOverride'}
overshadowing the file definition.
parseFile(filePath, options?)
:
Loads from a HOCON file, merges environment expansions, partial array merges, includes, plus optionaloverrides
. Typically used behind the scenes by the simplerparse(filePath, runtimeOptions)
.parseString(hocon, baseDir, options?)
:
Same logic, just from inline text. Great for dynamic or test configs.
The star of the show is parse(filePath, runtimeOptions?)
, which merges environment & CLI arguments automatically so your config can be manipulated by external factors with zero extra code.