-
Notifications
You must be signed in to change notification settings - Fork 2
English__Theory__Per Environment Configuration
This is one of the most required functionalities of a good configuration package: The ability to specify values that are different across different application environments. Very commonly found examples are:
- The database server name
- The database server's username
- The logging minimum level
- The hostname of an externally-hosted web service
The list can go on and on, but hopefully were are now in the same page.
The wj-config
package provides several ways to do this, grouped into 2 categories: Classic and Conditional.
This is what is found virtually anywhere else: You, as developer, provide the necessary logic during configuration construction to identify the sources pertinent to the current environment.
This is how classic per-environment configuration looks like with wj-config
:
import wjConfig, { buildEnvironment } from `wj-config`;
import mainConfig from './config.json' assert { type: 'json' };
const env = buildEnvironment(process.env.NODE_ENV);
const config = await wjConfig()
.addObject(mainConfig).name('Main')
.addObject(loadJsonFile(`./config.${env.current.name}.json`)).name(env.current.name)
.build();
export default config;
This code sample states that you, the developer, want to include the main source, and then another source that comes from reading a JSON file from disk whose name is dynamically constructed (by you, the developer).
It is called the classic method because this is what you find everywhere. You are tasked with the responsibility
of choosing the correct list of sources for your configuration. The configuration package does not care at all
whether you did the coding correctly or not. Is there a typo in the file name? The package doesn't care. What about
a typo in the value of the NODE_ENV
environment variable? The package doesn't care. Etc. You most likely got the
idea.
This is something not found anywhere. This is only found in wj-config
. This package has the ability to select
the appropriate data source as long as you provide your list of possible environments and will help you make sure you:
- Don't have typos in environment names.
- Don't forget to add at least one environment-specific data source for a possible environment.
The heart of conditional per-environment configuration is the forEnvironment()
function. Let's illustrate it with
some sample code:
import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from './config.json' assert { type: 'json' };
const myEnvs = [ 'Dev', 'PreProd', 'Prod' ];
const env = buildEnvironment(process.env.NODE_ENV, myEnvs);
const config = await wjConfig()
.includeEnvironment(env)
.addObject(mainConfig).name('Main')
.addObject(loadJsonFile('./config.Dev.json'))
.forEnvironment('Dev') // Automatically names the data source as "Dev (environment-specific)"
.addObject(loadJsonFile('./config.PreProd.json'))
.forEnvironment('PreProd') // Automatically names the data source as "PreProd (environment-specific)"
.addObject(loadJsonFile('./config.Prod.json'))
.forEnvironment('Prod') // Automatically names the data source as "Prod (environment-specific)"
.build();
export default config;
Here we are not worrying about dynamically building a configuration file name. We just type all of our possible
configuration files and then use the forEnvironment()
function to let the builder know to only apply it if we have
a matching environment name.
This forEnvironment()
function uses the builder's when()
function to condition the data source for you, so
all data sources can be freely specified without the need of any logic, hence the name conditional configuration.
By using forEnvironment()
we are telling the configuration builder that we want per-environment configuration, and
because the builder knows the list of all possible environments (we gave it via the environment object), then the
builder's build()
function, when executed, will make sure that every possible environment name has at least one
data source associated to it. If this is not the case, an error is thrown so you, the developer, may correct the
issue immediately, saving you headaches because you find out now about the missing data source, not after you
deploy to your environments, forcing you to recreate Docker images and whatnot.
Of course, if you ever have the need to disable this environment coverage check, you can by passing false
to the
build()
's second argument: .build(false, false)
Ok, so this conditional approach does have its benefits, but it is so wordy! Agreed. This is why the builder object
provides the addPerEnvironment()
function. It is a shortcut function because it gives you, the developer, the
opportunity to specify all per-environment data sources by using a single function.
This is how the previous example looks like when re-written to use addPerEnvironment()
:
import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from './config.json' assert { type: 'json' };
const myEnvs = [ 'Dev', 'PreProd', 'Prod' ];
const env = buildEnvironment(process.env.NODE_ENV, myEnvs);
const config = await wjConfig()
.includeEnvironment(env)
.addObject(mainConfig).name('Main')
// b: The builder object; e: One of the environment names.
// The provided function is called once for each of the defined environment names.
.addPerEnvironment((b, e) => b.addObject(loadJsonFile(`./config.${e}.json`)))
.build();
export default config;
This shortcut function brings the simplicity of the classic approach and the benefits of the conditional approach
together. addPerEnvironment()
accepts a function that receives two arguments: The builder object and an
environment name from the list of all possible environment names defined in the environment object. The function you
provide will be called once per environment name, effectively covering all possible environments and without the
need to risk a typo by repeating the environment names in code. Look closely: You only had the need to write the
environment names once, when defining the myEnvs
array.
The above conditional examples suffer from a performance hit: Even when only one environment-specific JSON file is needed, the conditional examples actually read and load in memory all JSON files for all environments. To account for this, the examples can be re-written as:
import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from './config.json' assert { type: 'json' };
const myEnvs = [ 'Dev', 'PreProd', 'Prod' ];
const env = buildEnvironment(process.env.NODE_ENV, myEnvs);
const config = await wjConfig()
.includeEnvironment(env)
.addObject(mainConfig).name('Main')
.addObject(() => loadJsonFile('./config.Dev.json'))
.forEnvironment('Dev')
.addObject(() => loadJsonFile('./config.PreProd.json'))
.forEnvironment('PreProd')
.addObject(() => loadJsonFile('./config.Prod.json'))
.forEnvironment('Prod')
.build();
export default config;
import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from './config.json' assert { type: 'json' };
const myEnvs = [ 'Dev', 'PreProd', 'Prod' ];
const env = buildEnvironment(process.env.NODE_ENV, myEnvs);
const config = await wjConfig()
.includeEnvironment(env)
.addObject(mainConfig).name('Main')
.addPerEnvironment((b, e) => b.addObject(() => loadJsonFile(`./config.${e}.json`)))
.build();
export default config;
The detailed explanation can be found here.
Contents
- English
- Theory
- JavaScript Concepts
- Data Sources
- Español
- Teoría