Configuration files are processed in a heiarchical manor in which a set of configurations from multiple sources are merged. This additionally includes:
- Runtime validation of configuration values against a provided schema.
- Coercion of environment variable values to expected types.
- Support for JSON and YAML file formats.
- NODE_ENV directed configuration.
- Customizing overrides
- Mapping from environment variables
With a given directory, by default, configuration files are loaded in the following manor (if found):
default.yaml -> ${NODE_ENV}.yaml -> ENV variables -> local.yaml
in which 'local' configuration has the final override.
config/default.yaml
:
baseUrl: http://localhost
port: 3000
database:
product_data:
uri: 'postgres://db_user:password@postgres/defaultdb'
options:
connectionTimeoutMillis: 100
idelTimeoutMillis: 100
config/production.yaml
:
database:
product_data:
uri: '${DB_URI}',
options:
connectionTimeoutMillis: 30000
idleTimeoutMillis: 5000
ENV
:
DB_URI="postgres://produser:[email protected]:172777/v6"
CONFIG_port=1234
config/local.yaml
:
port: 4159
The resulting configuration object loaded while NODE_ENV=production
:
{
baseUrl: 'http://localhost', // from defult.yaml
port: 4159, // from local.yaml
database: {
product_data: {
uri: 'postgres://produser:[email protected]:172777/v6', // mapped from ENV
options: {
connectionTimeoutMillis: 30000, // from production.yaml
idleTimeoutMillis: 5000 // from production.yaml
}
}
}
Notable dependencies:
- runtypes - The schema used to validate configuration values is based on runtypes. Not all possible type specifications are supported (YMMV). However, providing a runtype schema is not required.
Synchronously loads configurations based on provided options and returns an instance of Tconf.
import path from 'path';
import { Number, Record, String } from 'runtypes';
import {initialize} from 'tconf';
const schema = Record({
host: String,
}).And(Partial({
port: Number,
}));
const tconf = initialize({
path: path.join(__dirname, '..', 'config'),
schema,
});
const config = tconf.get();
server.start(config.host, config.port ?? 3000);
Path to directory, or set of paths for multiple directories, containing configuration files.
Single directory:
const tconf = initialize({
path: '../config'
})
Multiple directories:
const tconf = initialize({
path: [
'../config',
'../config/secret',
]
})
With the given example above with multiple directories, files will be iterated over in the following manor:
config/default.yaml
config/secret/default.yaml
config/${NODE_ENV}.yaml
config/secret/${NODE_ENV}.yaml
config/local.yaml
config/secret/local.yaml
Defaults to yaml
. Possible values 'yaml'
or 'json'
.
If provided, validation and value coercion will be performed. Supported types:
number
boolean
Date
RegExp
Array<string|number|boolean|Date|RegExp>
Prefix used to identify environment variables. Default: 'CONFIG_'
CONFIG_server__host='http://myserver.com'
# => { server: { host: 'http://myserver.com' } }
With override:
const tconf = initialize({
path: '../config',
envPrefix: 'CFG_',
});
const config = tconf.get();
// $ CFG_server__host='http://foo.com'
//
// => { server: { host: 'http://foo.com' } }
Path separator for nested configurations to use in env variables. Default: '__'
database:
options:
maxPoolSize: 5
override via env variable:
CONFIG_database__options__maxPoolSize=10
Defaults to 'overwrite'
. Possible values 'combine'
or 'overwrite'
.
Internally, tconf will deeply merge objects. By default, array properites are overwritten.
const a = { obj: [{ name: 'joe' }, { name: 'john' }, 2] };
const b = { obj: [{ lastName: 'jack' }, 1] };
deepMerge(a,b);
// => { obj: [{ lastName: 'jack' }, 1] }
This behavior can be changed so that array properties are merged by specifying an arrayMergeMethod
sub-option of 'combine'
. When using this, the values at the same index are merged if they are objects, otherwise concatenated if they are primitives.
const a = { obj: [{ name: 'joe' } , { name: 'john' }, 1 ] }
const b = { obj: [{ lastName: 'jack' }, , 2 ] }
deepMerge(a,b, { arrayMergeMethod: 'combine' })
// => { obj: [{ name: 'joe', lastName: 'jack' }, { name: 'john' }, 1, 2] }
mergeOpts usage:
const tconf = initialize({
path: '../config',
mergeOpts: {
arrayMergeMethod: 'combine'
}
})
const config = tconf.get();
JSON object that conforms to the schema
This is used as default configuration values. Particularly used for testing.
List of sources, in priority order, to process. These values are either base file names, or the tokens NODE_ENV
and ENV
.
By default, the sources are defined in the following order:
default
: Loads the file default.yamlNODE_ENV
: loads from a file with the same base name as NODE_ENV. For example,NODE_ENV=production
is translated asproduction.yaml
ENV
: loads configuration from variables prefixed withCONFIG_
local
: loads from the file local.yaml
Files are read across all specified paths in the same order. Sample override:
const tconf = initialize({
format: 'json',
path: ['config', 'config/secret'],
sources: ['base', 'NODE_ENV', 'ENV', 'local']
})
const config = tconf.get()
The above will result in an untyped config object that merges all configurations found under two different directories.
Registers a named configuration with associated schema. This will load, validate, and return the schema synchronously. This allows application modules to have their configuration managed by a common tconf instance.
Application modules may register configuration with tconf while using the same configuration sources.
First, initialize your global configuration. This will establish the common location and options for loading configuration.
// src/config.ts
import path from 'path';
import {initialize} from 'tconf';
import { Number, Record } from 'runtypes';
const Config = Record({
api: Record({
port: Number
})
})
// exporting this so modules can register their configuration
export const tconf = initialize({
path: path.join(__dirname, '..', 'config),
schema: Config
})
export default tconf.get(); // Static<typeof Config>
Then in your module, register your local schema against a unique name.
// src/modules/crypto/config.ts
import { tconf } from '../../config'
import { Record, String } from 'runtypes';
const Schema = Record({
key: String
})
const config = tconf.register('crypto', Schema); // Static<typeof Schema>
export default config;
Module configuration is mapped to a named section in configuration files.
# config/default.yaml
api:
port: 3000
crypto: # <-- module config
key: 6K0CjNioiXER0qlXRDrOozWgbFZ9LmG/nnOjl0s4NqM=
{% note %}
Note: Tconf will provide all configuration it finds and does not filter any out when requesting from the top level tconf.get()
. However, when strictly typing with Runtypes and TypeScript, other module configuration types are not exposed. In this way, your application code can act as if it doesn't exist though it literally does.
{% endnote %}
In some cases, it's preferred to specify deployment related configuration based on environment variables.
Environment variable mapping is supported by default through the use of a environment variable name prefix followed by a field path. The prefix and field delimeter are configurable, but default usage could look like:
CONFIG_some__field="value"
which would assign the value to:
{
some: {
field: "value"
}
}
To provide better interoperability between either existing environment variables or a non-path based variable naming conventions, variables can be directly mapped as interpolated values. For example:
database:
host: ${DB_HOST}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
The same merging and type coercion logic is applied. If any of the specified environment variables do not exist, the values will fall through to any specified defaults.
A default value may optionally be defined within the environment variable template. For example:
database:
host: ${DB_HOST:localhost}
username: ${DB_USERNAME:user}
password: ${DB_PASSWORD:"xSie:rJ39i023s"}
The default value follows after the :
delimiter. Double or single quotes surrounding the value are optional.
This library uses debug
. To print debug logging:
$ DEBUG=tconf* node ./run.js