Skip to content
/ brek Public

A powerful yet simple configuration library for Node.js. It’s structured, typed, and designed for dynamic configuration loading, making it perfect for securely managing secrets (e.g., AWS Secrets Manager). Written in TypeScript for safety and ease of use. Sponsored by Aeroview.

License

Notifications You must be signed in to change notification settings

mhweiner/brek

Repository files navigation

brek

(formerly lambdaconf)

build status SemVer Conventional Commits Static Badge

Brek stands for Blocking Resolution of Environment Keys.

Brek is a powerful yet simple configuration library for Node.js. It’s structured, typed, and designed for dynamic configuration loading, making it perfect for securely managing secrets (e.g., AWS Secrets Manager). Written in TypeScript for safety and ease of use. Sponsored by Aeroview.

🔒 Out-of-the-box Typescript support

  • Turn your runtime errors into safer compile-time errors! Automatically generated Typescript type definition for configuration object
  • Any override must satisfy Partial<DefaultConfig> type, or it will throw a Typescript error

😃 Simple, easy-to-use, safe, and stable

  • All settings are in simple, logic free .json files.
  • Adds structure and organization to your configuration files
  • Easily see what is being overridden and where
  • Comprehensive yet easy to understand documentation
  • Small, modular, and unit-tested codebase written in Typescript with no dependencies.

💪 Flexible & powerful

  • Differentiates between concepts such as environment, deployment, and user and provides an out-of-the-box solution with sensible merge strategy
  • Provides for overrides via CLI without polluting the CLI argument namespace
  • Fast. Runtime processing is done during app initialization only.
  • Put environment variables directly into .json files

🤖 Dynamic loading

  • Great for AWS Secrets Manager, AWS Parameter Store, HashiCorp Vault, or custom dynamic runtime functions
  • Any custom logic lives in loaders, keeping your config files logic-free
  • Provides an easy sharable and reusable plugin interface for sharing or re-use

Installation & Setup

1. Install from npm

npm i brek

2. Create conf directory

Create a directory called conf in the root of your project. This is where your configuration will go, along with the generated Conf.d.ts TypeScript Declaration File.

Note: If you want to use a different directory, you can set the BREK_CONF_DIR environment variable to the path of your configuration directory.

Here's an example conf folder:

root/
└── conf/
    └── deployments
        └── test.acme.json
    └── environments
        └── development.json
        └── production.json
    └── users
        └── john.json
    └── default.json

At a minimum, default.json is required at the root of your conf folder. To learn more about the other folders, see merge strategy and configuration rules

3. Create your configuration files

Here's a simple example:

default.json

{
  "postgres": {
    "host": "localhost",
    "port": 5432,
    "user": "pguser"
  }
  "port": 3000,
  "foo": {
    "bar": true
  }
}

At a minimum, default.json is required at the root of your conf folder.

See full configuration rules, merge strategy, and reference the example folder structure above. Also, don't forget to check out loaders for dynamic runtime configuration.

4. Typescript Configuration (tsconfig.json)

Make sure the generated conf/Conf.d.ts file will be picked up by your Typescript parser. One way to do this is by including it in your include directive like so:

  "include":[
    "src/**/*",
    "conf/Conf.d.ts"
  ],

If you're using ts-node, it might help to add the following:

"ts-node": {
  "files": true
}

5. Call brek when your configuration changes to generate the type declaration file

Whenever your default.json configuration changes, you'll need to run the brek command to build the type declaration file. For example, you could add the following to your package.json file:

{
  "scripts": {
    "prepare": "brek"
  }
}

To run this manually, you can run npx brek. This will generate the Conf.d.ts file in your conf folder.

6. Optional: Add generated files to .gitignore

You may want to add conf/Conf.d.ts and conf/conf.json to your .gitignore file to prevent them from being checked into source control.

Loading the configuration

Before you can read the configuration within your app, you must first load it. This step involves reading the files from disk, merging them, and resolving any loaders. You have two options:

  1. Use loadConf() within your app to load the configuration asynchronously before your app starts.

  2. Use loadConf() in a script to generate the conf.json configuration file before running your app.

Here's an example of using loadConf() in your app:

import {loadConf, getConf} from "brek";

loadConf() // optionally pass in loaders and options here
    .then(() => {
        const conf = getConf();
        console.log(conf);
        // start your server, etc.
    })
    .catch(console.log.bind(console));

Here's an example of using loadConf() in an init script:

import {loadConf} from "brek";

loadConf()  // optionally pass in loaders here
    .then(() => {
        console.log("Configuration loaded successfully and written to conf.json");
    })
    .catch(console.log.bind(console));
ts-node init.ts && ts-node src/index.ts

Getting the config object

Once loaded, use getConf to access the configuration object. The configuration is cached after the first load, so you can call getConf as many times as you want without worrying about performance.

Example:

import {getConf} from "brek";

const conf = getConf(); // type of Conf

console.log(conf); // logs config object

const isFooBarEnabled: boolean = conf.foo.bar; // will throw Typescript error as expected if does not exist or is not a boolean

If you need the type interface, you can import it:

import {Conf} from "brek";

Configuration rules

  • default.json is required, everything else is optional. Recommended practice is that default.json contains all of your "local development" settings.

  • All configuration files must be a subset of default.json. Think of them simply as overrides to the default. In Typescript terms, conf files must be of type Partial<Conf>.

  • A property's type should not change simply because of a different environment, user, or deployment. This is basically saying the same as above.

  • Loaders always must return a string. If you need to return a different type, you can use JSON.parse() or similar.

  • Arrays should be homogenous (not of mixed types).

Configuration merge strategy

Configurations are merged in this order, with the later ones overriding the earlier ones:

  1. default.json
  2. environment file
  3. deployment file
  4. user file
  5. CLI/env overrides

Which of these sources to choose depends on the presence of certain process.env configuration variables:

process.env conf file
NODE_ENV /conf/environments/[NODE_ENV].json
DEPLOYMENT /conf/deployments/[DEPLOYMENT].json
USER /conf/users/[USER].json
BREK N/A
OVERRIDE (deprecated) N/A

A few notes:

  • BREK must be valid JSON. Learn more
  • USER is usually provided by default by UNIX environments (try console.log(process.env.USER))
  • Loaders parameters are simply replaced, not merged. A loader instance is treated as a primitive.
  • Arrays are simply replaced, not merged.

Using CLI/env overrides

You can use the BREK environment variable to override properties via CLI/ENV. BREK must be valid JSON. Using jq simplifies dynamic JSON construction, ensures proper quoting, and makes it easier to handle environment variables.

Examples

# Override the a.b property
BREK=$(jq -n '{a: {b: "q"}}') ts-node src/index.ts

# Override the postgres property wth an environment variable
DATABASE_URL="postgres://user:pass@localhost:5432/db"
BREK=$(jq -n --arg db "$DATABASE_URL" '{postgres: $db}') ts-node src/index.ts

Environment variables in config files

You can use environment variables as values by wrapping it in ${...}. For example, to use environment variable FOO, use ${FOO}. This will translate to process.env.FOO. These will always be typed as strings. Example config file:

{
  "foo": "${FOO}"
}

Loaders

Loaders are custom functions that are called during startup (run-time). This can be used to do anything, such as fetching secrets from AWS Secrets Manager, or any other dynamic runtime operation. They can be Promise/async/await based.

Example

conf/default.json

{
  "foo": {
    "[fetchSecret]": {
      "key": "demo"
    }   
  },
  "bar": {
    "[add10]": 42
  }
}

index.ts

import {loadConfig, getConf} from "brek";

const loaders = {
    fetchSecret: (params: {key: string}) => Promise.resolve(`secret_${a}`),
    add10: (val: number) => String(val + 10),
};

loadConfig(loaders)
    .then(() => {
        console.log(getConf().foo); // "secret_demo"
        console.log(getConf().bar); // "52"
        //start server, etc.
    })
    .catch(console.log.bind(console));

Usage

Loader functions must extend (params: any) => string. If helpful, you can import the Loader type like so:

import type {Loader} from 'brek';

In a conf file, any object with a single property matching the pattern wrapped in square brackets ([...]) is assumed to be a loader. The key is the loader name, and the value is the parameter passed to the loader.

If a matching loader is not found, it will throw a LoaderNotFound error. Loaders must return strings.

API

loadConf(loaders?: Loaders): Promise<void>

Loads the configuration files from disk, merges them, resolves any loaders, and writes the final configuration to conf.json. This function must be called before getConf().

  • loaders (optional): An object containing loader functions. The key is the loader name, and the value is the loader function.

getConf(): Conf

Returns the configuration object. This function must be called after loadConf().

Recommended best practices

  • default.json should contain all of your local development settings, and then "progressively enhance" from there.
  • Use AWS Secrets Manager or Hashicorp Vault to store your sensitive information and use a loader to load them.

Debugging

You can set the BREK_DEBUG environment variable to see debug output. Example:

BREK_DEBUG=1 ts-node src/index.ts

Use with caution! This may output sensitive information to the console.

Known issues

  1. Some IDEs (particularly IntelliJ/Webstorm) occasionally have some issues with caching of the generated Conf.d.ts file (which is stored in your conf folder). If you run into this problem, restarting your TS service.

Support, feedback, and contributions

  • Star this repo if you like it!
  • Submit an issue with your problem, feature request or bug report
  • Issue a PR against main and request review. Make sure all tests pass and coverage is good.
  • Write about this project in your blog, tweet about it, or share it with your friends!

Sponsorship


Logo

Aeroview is a lightning-fast, developer-friendly, and AI-powered logging IDE. Get started for free at https://aeroview.io.

Want to sponsor this project? Reach out.

Other useful libraries

  • autorel: Automate semantic releases based on conventional commits. Similar to semantic-release but much simpler.
  • hoare: An easy-to-use, fast, and defensive JS/TS test runner designed to help you to write simple, readable, and maintainable tests.
  • jsout: A Syslog-compatible, small, and simple logger for Typescript/Javascript projects.
  • cjs-mock: NodeJS module mocking for CJS (CommonJS) modules for unit testing purposes.
  • typura: Simple and extensible runtime input validation for TS/JS, written in TS.

License

MIT

About

A powerful yet simple configuration library for Node.js. It’s structured, typed, and designed for dynamic configuration loading, making it perfect for securely managing secrets (e.g., AWS Secrets Manager). Written in TypeScript for safety and ease of use. Sponsored by Aeroview.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published