Adapting 12 factor app configuration to a type checked, application focused world view.
Features
- Hierarchical configuration - values are merged from multiple sources.
- Supported file formats: yaml, json, json5
- Environment specific configuration via NODE_ENV
- Runtime type validation.
- Support for modulare configuration.
- Type coercion of environment variables - string values can be converted to:
number
boolean
Date
RegExp
Array<number|boolean|Date|RegExp>
- All values can implicitly be configured by environment variables.
Overview
12 factor app guidelines for configuration promotes "strict separation of config from code" through the use of environment variables. While this is beneficial from a deployment perspective, how this is implemented in many cases falls short of adequately supporting complex configuration within an application.
Typical approaches involve referencing process.env
directly, perhaps with additional support through a library like dotenv. These applications often start by working with a flat list of variables.
const {
DB_HOST,
DB_USERNAME,
DB_PASSWORD,
// ...
} = process.env;
As configuration becomes more complex, this flat structure becomes cumbersome to deal with and to reason about. To combat this, developers will organize their configuration into a hierarchical structure. Having to map from a flat list of env vars into a desired shape, performing type coercion from env var strings, and executing validation is often an exercise left for the developer. For example, a desired end state for your configuration might look like:
api: {
baseUrl: string
port?: number
debugMode?: boolean
auth: {
secret: string
}
}
database: {
host: string
username: string
password: string
driverOpts?: {
connectionTimeout?: number
queryTimeout?: number
}
}
...
Representing this as a flat list of env vars is not an effective way to work with your configuration. tconf addresses this by allowing authors to specify the desired shape and type of their configuration and performs mapping and coercion from environment variables automatically.
Getting Started
1. install
npm install tconf
2. create config specification (optional)
tconf utilizes runtypes for runtime type checking and as a schema for your config. This represents what you want your config to look like.
// src/config.ts
import { Boolean, Optional, Record, Static, String } from 'runtypes';
const ApiConfig = Record({
port: number,
debug: Optional(Boolean)
})
const DatabaseConfig = Record({
host: String,
username: String,
password: Optional(String)
})
const Config = Record({
api: ApiConfig,
database: DatabaseConfig
});
export type Config = Static<typeof Config>;
where the type Config
is inferred as:
interface Config {
api: {
port: number
debug?: boolean
},
database: {
host: string
username: string
password?: string
}
}
If you aren't using TypeScript or don't care about having your configuration statically typed, coerced, and validated then you can skip this.
3. map to env var names (optional)
Create a config file that defines a mapping of env vars. tconf provides support for template variables that can be used for env var interpolation (similar to docker compose) and also allows for assigning default values.
# config/env.yaml
api:
port: ${API_PORT:3000}
database:
host: ${DB_HOST:"db.domain.com"}
username: ${DB_USER}
password: ${DB_PASSWORD}
This is also optional. tconf natively supports configuration mapping from environment variables following a path naming convention. (you can set any configuration value using an environment variable). Use interpolation variables in your config only if you need to map from some specifically named variable that doesn't match your config.
4. load your configuration
// src/config.ts
import { initialize } from 'tconf'
const tconf = initialize({
// directories containing configuration files
path: path.join(__dirname, '..', 'config'),
// the runtypes Config object (optional)
schema: Config,
// sources to look for config, in this case the files
// default.yaml, ${NODE_ENV}.yaml, and env.yaml
sources: ['default', 'NODE_ENV', 'env'],
})
export default tconf.get();
tconf will import configurations from the defined sources (or a set of defaults) from the specified directories, and merge the values in the order of the specified sources.
5. use it
// src/foo.ts
import config from './config'
import dbConnect from './db'
const conn = await dbConnect(config.database);
6. use in isolated modules
Within larger applications, you may want to isolate certain areas of your code into modules. It may make sense to isolate your configuration to such modules as well.
First, expose your initialized Tconf instance:
// src/config.ts
import { initialize } from 'tconf'
export const tconf = initialize({ // <-- export the instance
// ...
})
export default tconf.get(); // exports the configuration
Then in your module, register your configuration schema and provide access to your module.
// src/modules/db/config.ts
import {tconf} from '../../config'
const Config = Record({
uri: String
})
const config = tconf.register('database', Config); // Static<typeof Config>
export default config
The configuration will be sourced the same way, but you'll need to add your configuration under the registered name.
# config/default.yaml
api:
# //...
database:
uri: postgresql://host.com:5432/appdb
Documentation
Please see the documentation for more detailed information and capabilities of tconf.