confederation
v0.0.6
Published
Manage configuration sources in your app like a boss! `.env` files, CLI flags, database rows—into a single, predictable object. Confederation normalizes keys, nests into objects by separators (by default `__` or `--`), and deeply merges sources so your ap
Readme
Confederation
Manage configuration sources in your app like a boss! .env files, CLI flags, database rows—into a single, predictable object. Confederation normalizes keys, nests into objects by separators (by default __ or --), and deeply merges sources so your application can operate with one source of truth.
This zero-dependency helper focuses purely on normalization, nesting, and merging; it intentionally skips schema validation or type coercion. Every emitted value is either an nested object or a string.
We recommend running the unified config output through Zod or another validator/transformer afterwards.
Why another config helper?
Real-world Node.js services rarely rely on just one configuration channel. By the time you mix process.env, .env files, CLI overrides, deployment-specific secrets, and serialized preferences, you need a consistent way to normalize naming conventions and merge them without losing structure. Confederation focuses on this single job:
- Normalize keys from any naming convention (underscores, dashes, mixed case) into predictable camelCase paths.
- Nest hierarchical keys via configurable separators (
__,--, or your own pattern). - Merge multiple sources deeply, while still letting later sources override earlier ones.
- Type your configuration by passing a generic to
unifyConfigurations, so the final object is fully typed in TypeScript.
Installation
npm install confederationQuick start
Create a list of sources, optionally specify a parser per source, and let Confederation do the rest:
import { parsers, unifyConfigurations, utils } from "confederation";
import { parseArgs } from "node:util";
import dotenv from "dotenv";
type AppConfig = {
module: {
variable1: string;
variable2: string;
variable3: string;
};
};
const configurationSources = [
{
value: dotenv.config().parsed,
},
{
value: utils.pick(process.env, ["MODULE__VARIABLE2", "MODULE__VARIABLE3"]),
},
{
value: parseArgs({
args: process.argv.slice(2),
strict: false,
allowPositionals: false,
}).values,
parser: parsers.dashParser,
},
];
const config = unifyConfigurations<AppConfig>(configurationSources);
console.log(config.module.variable2);
// → "from-env"unifyConfigurations will:
- Run each source through
utils.nestKeyValuesusing the provided parser (defaults toparsers.underscoreParser). - Merge all normalized objects from left to right using the
defaultshelper, so later sources win.
Understanding sources
Each entry in the configurationSources array implements the UnifyConfiguration interface:
type UnifyConfiguration = {
value: Record<string, unknown>;
parser?: NestKeyValuesOptions;
};value– the raw key/value map from your config source.parser– optional instructions describing how to split and transform keys.
Leave parser undefined to use the default underscore parser (MODULE__FEATURE__FLAG_A → module.feature.flagA).
Built-in parsers
Confederation ships with two helpers inside parsers:
underscoreParser–nestifyPattern: "__"(ideal for env vars).dashParser–nestifyPattern: "--"(handy for CLI flags like--server--port=8080).
Parsers reuse the NestKeyValuesOptions type, so anything with a nestifyPattern string and a keyTransformer function will work.
Custom parser example
const colonParser: NestKeyValuesOptions = {
nestifyPattern: "::",
keyTransformer: (key) => key.toLowerCase(),
};
const config = unifyConfigurations([
{
value: { "db::PRIMARY::HOST": "db.internal" },
parser: colonParser,
},
]);Deep merges with defaults
Under the hood, Confederation relies on a small defaults helper that deeply merges plain objects, replaces arrays/classes, and protects against prototype pollution. You can also import it directly:
import { defaults } from "confederation";
const merged = defaults(
{ cache: { ttl: 60 } },
{ cache: { ttl: 120, host: "redis" } },
{ featureFlags: { preview: true } },
);defaults never mutates its arguments and always returns a new object.
Recommendations
- Order your sources from lowest to highest precedence (defaults first, env vars last) so the override chain is obvious.
- Co-locate parser definitions with the source they belong to—CLI args might require a dash parser, while env vars stick with underscores.
- Validate the unified output with Zod, Valibot, or any other schema validator/transformer before injecting it into the rest of your app.
That’s all you need to federate configuration across your application. Have ideas or found an edge case? Issues and PRs are welcome!
