Skip to content

English__Theory__Introduction

Ramirez Vargas, José Pablo edited this page Nov 7, 2024 · 3 revisions

Introduction

wj-config is a JavaScript configuration module for browser and server projects that works like .Net configuration where any number of data sources are merged and environment variables can contribute/overwrite values by following a naming convention.

Important Notes

Read the following notes carefully. Your project may not be suitable for this configuration package if it does not adhere to modern JavaScript/NodeJS standards.

  1. The package has been developed using TypeScript and transpiled to ES Modules. Your project must support the use of ES Modules.
  2. Building the configuration object is an asynchronous operation, so top-level await is highly recommended.
  3. The minimum supported NodeJS version is: NodeJS Minimum Version

Installing wj-config

Use either yarn or npm to install wj-config:

npm i wj-config

yarn add wj-config

How wj-config Works

This configuration package mimics the .Net configuration system found in .Net: A series of data sources are defined in a specific/desired order. These data sources are then merged (following the desired order) to create one single source of hierarchical configuration data.

There are many data source types available to you out of the box:

Data Source Add With Description
Object addObject() Adds the provided object as a data source.
Dictionary addDictionary() Adds the provided flat object as a data source.
Environment addEnvironment() Adds the provided flat object as a data source, assuming it follows the environment naming convention.
Fetched Object addFetched() Adds the object fetched from the provided URL as a data source.
JSON addJson() Adds the provided JSON string as a data source.
Single Value addSingleValue() Adds the provided key and value pair as a data source.

You are free to use any of these data sources in any order that you want or need. There is also no limit to the number of data sources you include. Add sources until your heart is content.

Building Your Configuration Module

Once the package is installed, you can build your configuration object basing it on any number of data sources of your choice. Usually you'll have a main configuration JSON file, then some per-environment JSON files, and maybe some data coming from environment variables (typical in server-sided JavaScript projects).

The idea here is to start a builder object that is used to specify your data sources in order of ascending priority, because the order matters: The data sources added last have a higher priority over the data sources that were added earlier. Never forget this rule.

NodeJS Example

The following is a typical NodeJS example written in ES Modules. The CommonJS version is shown later and is discouraged. Why? Because it complicates things. Building the configuration object is an asynchronous operation (as staed in the important notes), and because CommonJS does not (and never will) support top-level awaits, you will need IIFE's, promise chaining or something similar.

import wjConfig from "wj-config";
import mainConfig from './config.json' assert { type: 'json' };

const myListOfEnvironments = [
    'Dev',
    'Uat',
    'Prod'
];

const config = await wjConfig()
    .includeEnvironment(process.env.NODE_ENV, myListOfEnvironments)
    .addObject(mainConfig).name('Main')
    .addPerEnvironment((b, e) => b.addObject(() => loadJsonFile(`./config.${e}.json`)))
    .addEnvironment(process.env)
    .build();

export default config;

NOTE: The loadJsonFile() function is not something that exists in NodeJS. Create this function using Node's fs module.

This is what is happening here:

  1. The main function wjConfig is imported.
  2. The base project configuration JSON file ./config.json is imported.
  3. The list of possible environment names is defined. This is important and helpful. Read The Environment Object for details.
  4. The wjConfig() function is executed in order to obtain the builder object.
  5. The builder is given the environment information: The current environment, coming from the NODE_ENV environment variable, and the list of possible environment names.
  6. The base configuration data is added using addObject(); the name Main is assigned to it for tracing purposes.
  7. The special shortcut function addPerEnvironment() is used to add data sources for each of the possible environment names given to the builder object.
  8. The list of environment variables available through process.env are added as the environment data source.
  9. The builder is instructed to build the final configuration object.
  10. The final configuration object is exported.

Because later data sources have the potential to override data in earlier data sources, the order in which the data sources are specified is important. The order shown in the example is the typical one:

  1. A main, or base, data source that contains the most common settings across all environments.
  2. A per-environment data source that does specific overrides (or even data additions) where needed, such as a database server name.
  3. Environment variables that can be used to override any setting without the need to change the configuration JSON files, and therefore avoid re-building the application in any way, such as re-creating a Docker image.

The following are typical contents for the configuration JSON files. Note that you can group and sort your configuration values any way you want. The hierarchy is yours to define.

Main (or base) configuration:

{
    "app": {
        "title": "My NodeJS App",
        "version": "v1.0.0"
    },
    "database": {
        "server": "my-non-prod-server.example.com",
        "port": 1433,
        "trustedConnection": false,
        "userId": "sys-id"
    },
    "logging": {
        "minLevel": "information"
    }
}

Overrides required for the Dev environment (the developer's PC):

{
    "database": {
        "server": "localhost",
        "trustedConnection": true
    },
    "logging": {
        "minLevel": "verbose"
    }
}

Overrides required for the Prod environment:

{
    "database": {
        "server": "my-prod-server.example.com",
        "port": 3223
    },
    "logging": {
        "minLevel": "warning"
    }
}

Using the above JSON files as configuration sources would yield this as the final configuration object when the NODE_ENV environment variable is set to "Dev":

{
    "app": {
        "title": "My NodeJS App",
        "version": "v1.0.0"
    },
    "database": {
        "server": "localhost",
        "port": 1433,
        "trustedConnection": true,
        "userId": "sys-id"
    },
    "logging": {
        "minLevel": "verbose"
    }
}

And it would look like this for "Prod":

{
    "app": {
        "title": "My NodeJS App",
        "version": "v1.0.0"
    },
    "database": {
        "server": "my-prod-server.example.com",
        "port": 3223,
        "trustedConnection": false,
        "userId": "sys-id"
    },
    "logging": {
        "minLevel": "warning"
    }
}

Import config from any other module in your project and use it at will.

import config from "./config.js";

console.log('Application Title: %s', config.app.title);

IMPORTANT: In order to support you, the developer, the current environment's name must exist in the list of possible environment names. If you provide a current environment's name that is not found in the list of possible environments, a runtime error will be thrown. This protects you from typos.

Ok, without further ado, here's the CommonJS version of the config.js module:

const myListOfEnvironments = [
    'Dev',
    'Uat',
    'Prod'
];

module.exports = (async function() {
    const { default: wjConfig } = await import('wj-config');
    return wjConfig()
        .includeEnvironment(process.env.NODE_ENV, myListOfEnvironments)
        .addObject(mainConfig).name('Main')
        .addPerEnvironment((b, e) => b.addObject(() => loadJsonFile(`./config.${e}.json`)))
        .addEnvironment(process.env)
        .build();
})();

This does the same thing, but because CommonJS modules won't allow top-level awaits, the whole thing needs to be enclosed inside an asynchronous function, and then said function is converted into an IIFE (Immediately-Invoked Function Expression) and finally evaluated (as the first "I" in IIFE implies).

Usage is also different:

const configPromise = require('./config');

async function init() {
    const config = await configPromise;
    console.log('Application Title: %s', config.app.title);
}

init();

Browser Example

This example would be for browser-based projects. It works for React, Svelte, Vue and many others.

import wjConfig from "wj-config";
import mainConfig from './config.json';

const myListOfEnvironments = [
    'Dev',
    'Uat',
    'Prod'
];

const config = await wjConfig()
    .includeEnvironment(window.env.APP_ENV, myListOfEnvironments)
    .addObject(mainConfig).name('Main')
    .addPerEnvironment((b, e) => b.addFetched(`/config.${e}.json`))
    .addEnvironment(window.env)
    .build();

export default config;

It is basically identical to the NodeJS ES Modules version structure-wise. The differences are:

  1. There is no loading of configuration JSON files from disk. The environment-specific files are fetched instead.
  2. There is no process.env anywhere in browser projects. We have replaced it with window.env.
  3. The "environment variable" holding the environment name is named APP_ENV.

The only mystery here is that window.env thing because this is not a standard feature in web browsers. This window.env is an opínionated way of adding the concept of environment variables to a browser-based project. It works by creating a simple JS file that defines window.env:

window.env = {
    APP_ENV: 'Dev'
};

This file would be exchanged during application deployment with one that defines the desired environment name. There are probably many ways of doing this and the method selected would have to comply with whatever CI/CD the project uses.

If you would like to read about a way of doing this in Kubernetes, head to the deployment folder in this repository.

This env.js file would be included using a <script> HTML tag in all pages of the application. If the project is a SPA (Single Page Application), then just add it to the index page.


This would be the end of the introduction. This has covered only the very basics of what this configuration package offers. There are many other advanced features to explore:

  • Automatic URL-building functions.
  • Adding configuration values using environment variables.
  • Environment object to obtain information about the current environment.
  • Conditionally adding data sources.
  • Per-trait configuration to avoid creating multiple combinations of environment and per-XXX (region, tenant, language, etc.) configuration files.
  • Other ways to do per-environment configuration for fine control.
  • Data source tracing as a debugging tool for configuration values.

Keep reading the Wiki to master this package.

TypeScript in v3.0.0

NEW! Version 3.0.0 of this package is a full re-write of the TypeScript provided within it. As a result, the final configuration object is now 99% accurate. The changes, however, are extensive and TypeScript projects need to know a few specific things on how to use and type things while building the configuration object.

Head to the new TypeScript and wj-config Wiki page for details.