Skip to content

ngryman/fauda

Repository files navigation

Fauda

Configuration made simple.

FeaturesGetting StartedHow does it work?UsageAPICLIFAQ


Features

Fauda is an all-in-one library that:

  1. loads options from multiple sources: env vars, CLI options, and configuration files.
  2. merges them together in one unified configuration object.
  3. normalizes it by validating against a JSON schema and setting default values.

It offers the following advantages:

  • Simple - a single dependency to load, merge, and validate your configuration.
  • Flexible - multiple file formats support out of the box such as JSON, YAML, JavaScript, and even Typescript!
  • Reliable - a unique source of truth defined in a JSON schema.
  • Typescript friendly - generated typings for your code and configuration files (bonus: auto-completion in VSCode).

Getting Started

1️⃣ Install Fauda
npm install fauda
2️⃣ Set up your JSON schema

Fauda uses a JSON schema to load and normalize your configuration.

Create a schema.json file:

{
  "$schema": "http://json-schema.org/draft-07/schema",
  "title": "My awesome app configuration",
  "type": "object",
  "properties": {
    "$schema": {
      "description": "Path to my app's schema.",
      "type": "string"
    },
    "port": {
      "description": "The port the server listens to.",
      "type": "number",
      "default": 3000
    },
    "open": {
      "description": "Open in a browser tab if true.",
      "type": "boolean",
      "default": false
    },
    "mode": {
      "description": "Mode of the app.",
      "type": "string",
      "enum": ["development", "production"],
      "default": "${NODE_ENV}"
    },
    "publicPages": {
      "description": "A list of public pages.",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["publicPages"]
}

For more information on JSON schemas, you can take a look at their Getting Started guide.

3️⃣ Generate types (optional)

Generating types allows you to have a strongly typed configuration object in your code. As a bonus, it also enables autocompletion for Typescript configuration files!

Generate a src/configuration.ts file:

$ npx fauda types

This will generate the following file:

export interface Configuration {
  port?: number
  open?: boolean
  mode?: 'development' | 'production'
  publicPages: string[]
}

For more information about generating types, please take a look at the CLI section.

4️⃣ Load & validate your configuration.

Assuming your package's name is my-app:

import { fauda } from 'fauda'
import { Configuration } from './configuration'

async function loadConfiguration() {
  try {
    const configuration = await fauda<Configuration>('my-app')
  } catch (err) {
    console.error(err.message)
  }
}

How does it work?

Fauda loads your configuration from several sources using the following order of precedence: environment variables > CLI options > configuration files.

Option names are inflected according the source's typical naming convention:

Source Casing Example
Configuration file camel publicPages
CLI options kebab --public-pages
Environment variable upper+snake MY_APP_PUBLIC_PAGES

Once your configuration is loaded, Fauda normalizes it into a valid configuration object that your library / application can use. The normalization process validates your configuration using the provided JSON schema. It checks that the type of options are valid, required options are specified, sets default values, and also expand environment variables references!

🙋🏻‍♂️ What is environment variable expansion?

You can reference an environment variable name's as your option's value. Fauda will replace its value at runtime, giving you the opportunity to depend on any environment variable in your configuration.

For instance, if you have a mode option that varies depending on the NODE_ENV's value, you can do it like this:

"mode": {
  "default": "${NODE_ENV}"
}

Note that you can also reference environment variables in your JSON schema using the default value:

"mode": {
  "description": "Mode of the app.",
  "type": "string",
  "enum": ["development", "production"],
  "default": "${NODE_ENV}"
}

Usage

Configuration files

Fauda first searches for a config.${myApp} property in the package.json file of your users. If not found, it then searches for a various configuration files, starting from the current directory up to the root.

Here is a list of the configuration file names and formats that are supported:

File Format
.${myPackage}rc json
${myPackage}.config.js js
${myPackage}.config.json json
${myPackage}.config.ts ts
${myPackage}.config.yaml yaml
${myPackage}.config.yml yaml
.config/${myPackage}rc json
.config/${myPackage}.config.js js
.config/${myPackage}.config.json json
.config/${myPackage}.config.ts ts
.config/${myPackage}.config.yaml yaml
.config/\${myPackage}.config.yml yaml

CLI options

Fauda parses CLI options as you can expect from any other argument parsers!

Options are "kebab-"cased. For instance, the publicPages option is transposed as the --public-pages CLI argument.

🙋🏻‍♂️ What about arrays?

Arrays are supported in two ways.

  1. Declare a JSON-compatible array as value.
  2. Use the same argument multiple times.

Here's an example that gives the same result:

$ my-app --public-pages=/home --public-pages=/about
$ my-app --types="['/home', '/about']"

Environment variables

Fauda parses environment variables prefixed with your package's name. This is precaution to avoid name clashes with other application or system-wide environment variables.

For instance, if your package's name is my-app, Fauda will parse variables with the MY_APP_ prefix.

🙋🏻‍♂️ What about arrays?

Arrays are supported! You simply need to declare a JSON-compatible array wrapped between quotes.

Here's an example:

$ MY_APP_PUBLIC_PAGES="['/home', '/about']"

API

Loads, normalizes, and returns a configuration object from multiple sources.

Params

  • namespace string: String used to prefix environment variables and namespace your configuration in the package.json. It's typically your package's name.
  • schema string | JsonObject: Path to a JSON schema, or the schema itself, used to normalize your configuration.
  • options? Partial<FaudaOptions>: See available options.

Examples

import { fauda } from 'fauda'

const configuration = await fauda('my-app', 'schema.json')
Option Type Default Description
args string[] process.argv Array of CLI options, used by the CLI options loader.
env NodeJS.ProcessEnv process.env Dictionary of environment variables, used by the environment variables loader.
cwd string process.cwd() Array of CLI options, used by the configuration files loader.

Normalizes a configuration object using the provided JSON schema.

Params

  • config: JsonObject: Configuration object to normalize.
  • schema: string | JsonObject: Path to a JSON schema, or the schema itself.
  • env?: NodeJS.ProcessEnv: (default: process.env) An object containing environment variables, used to expand variables in the schema.

Examples

import { normalize } from 'fauda'

const schema = {
  type: 'object',
  properties: {
    foo: {
      type: 'string',
      default: 'bar'
    }
  }
}

const safeConfig = await normalize({}, 'schema.json')
// { foo: 'bar' }

Returns the Configuration type definition inferred from the given JSON schema.

Params

  • schema string | JsonObject: Path to a JSON schema, or the schema itself, used to generate the Configuration type.
  • options? Partial<Options>: See available options of json-schema-to-typescript.

Examples

import { promises as fs } from 'fs'
import { generateTypes } from 'fauda'

const source = await generateTypes('schema.json')
await fs.writeFile('src/configuration.ts', source)

CLI

Generate types

Fauda comes with a CLI utility that automatically generates types from your JSON schema. This is especially useful to provide a strongly typed configuration object to your Typescript project, but also to provide auto-completion for your users using a .ts configuration file.

By default, the types command will read a schema.json file and write the output to a src/configuration.ts file:

You can change these defaults with the following CLI options:

  • --input, -i: Path to a JSON schema file.
  • --output, -o: Path to the output Typescript file.

Examples

$ fauda types
$ fauda types -i my-schema.json -o src/types/configuration.ts
🙋🏻‍♂️ How can I integrate this in my workflow?

Typescript projects generally have a build script to transpile sources to plain JavaScript. You can generate your types right before using with the prebuild script:

{
  "scripts": {
    "build": "tsc",
    "prebuild": "fauda types"
  }
}

Usually you will also want to watch for changes to your schema and reflect these changes in the generated types to benefit from your IDE's automcompletion.

Assuming you have dev script that watches for changes, you could split your scripts to transpile your code and generate your types in two different scripts, and use a tool like npm-run-all to run them sequentially:

{
  "scripts": {
    "dev": "run-s dev:*",
    "dev:types": "fauda types",
    "dev:build": "tsc -w"
  }
}

FAQ

Why not supporting nested options?

Fauda only supports a flat options object, simply because it's easier to manipulate and to reason about. In my opinion, nested options are usually neither a good idea as it makes merging default options harder, nor necessary as one can express a sense of hierarchy using "dotted names" instead (eg. cooking.time) or just camel case (eg. cookingTime).