Writing Extensible Configurations with Ternary Conditions
Working in industries where security was a top priority, and collaborating with friends and colleagues who always kept security in mind, writing a configuration parser that allowed for different types of input seemed like a necessity.
Recently, I decided it would be good to have a script that I could quickly copy over to another project and edit as needed with a base template in mind. Here’s a small example:
// config.js
'use strict'
var configure = function() {
try {
var cfg = require('../config.json');
} catch(err) {
var cfg = {};
}
return {
app:
{ port: process.env.APP_PORT || ((cfg.app || {}).port) || 8000
, namespace: process.env.APP_NAMESPACE || ((cfg.app || {}).namespace) || '/api'
, host: process.env.APP_HOST || ((cfg.app || {}).host) || 'localhost'
}
, mongo:
{ host: process.env.MONGO_HOST || ((cfg.mongo || {}).host) || 'localhost'
, user: process.env.MONGO_USER || ((cfg.mongo || {}).user) || 'root'
, pass: process.env.MONGO_PASS || ((cfg.mongo || {}).pass) || ''
, port: process.env.MONGO_PORT || ((cfg.mongo || {}).port) || 3336
, database: process.env.MONGO_DATABASE || ((cfg.mongo || {}).database) || ''
}
, redis:
{ host: process.env.REDIS_HOST || ((cfg.redis || {}).host) || 'localhost'
}
};
}
module.exports = configure;
Running this script pulls information from several possible sources, in the following order of hierarchy:
- Environment Variables
- JSON config
- Default value
This allows for a few different use cases. In the case of a production usage, a config can be pulled directly from pre-set environment variables. This also allows for a dev to store a copy of a development config that won't be stored in source control. On a system with a fresh install (potentaly a Docker or Vagrant dev environment) you can set default values that will be used if an ENV var or config file/option is not found.
Breakdown#
'use strict'
var configure = function() {
try {
var cfg = require('../config.json');
} catch(err) {
var cfg = {};
}
return {
app:
{ port: process.env.APP_PORT || ((cfg.app || {}).port) || 8000
}
};
}
module.exports = configure;
We're setting the environment into strict mode and creating a named function
'use strict'
var configure = function() {
// Code Here
}
// Export the function to be called by index
module.exports = configure;
Within that function first we attempt to pull in the config.json. (I normally store my config.js in a lib folder and config.json in the project root)
try {
var cfg = require('../config.json');
} catch(err) {
var cfg = {};
}
This attempts to pull in the config.json and set it to cfg. If the file is not found it create an object literal
From here we parse the ENV and config.json. If neither are found the default is used and sent stored within the object.
Working Inside Out#
Within the second example config we're looking to set the apps port. We need to check to see if the ENV var is set, if that is not set we then go to the config, but there's a catch. If the config does not exist, we need to fake the existance of any nesting necessary to access that value. When I create my configs, I have use a few different sections depending on different aspects of the application. Core application configurations live within the app sub-object and normally use the same very limited configs. For scaling though, we can add addition application specific items in this app object. Some sort of database (this may change depending on the project of course), followed by thrid party resources (API keys).
Faking Subobjects#
Since in this example we set cfg = {}, when the config is not pulled in we get to skip a step. Otherwise we would have
to start with the following code within each query (cfg || {})
This will check for the cnf.app to exist if it doesn't it will create an object literal {}. The reason we are setting
this to undefined is that if the ENV variable is not set, it will check this next. If this does not exist, we need to
proceed to the default setting.
Set Your Heirarchy#
For this specific setup, we want our ENV variables to be the highest priority, followed by anything within config.json,
and when all else fails, we need to use the default. So we add a ( around the front of this ternary and at the end
add || 8000)
(process.env.APP_PORT || ((cfg.app || {}).port) || 8000)
Which, when returned, as default would return this JSON object
console.log(configure());
Now to put it into action#
Above we have our config. Now lets make an express app that uses that conf to set the port
(function(){
'use strict'
require('express-namespace');
var app = require('express')();
var config = require('./lib/config')();
app.namespace(config.app.namespace, function() {
app.get('/', function(req, res) {
res.send('Hello World!');
});
});
app.listen(config.app.port, function() {
console.log('Listening on %d, at %s', config.app.port, config.app.namespace);
});
})();
When run, if no config or environment variables are present, this should log to the screen
$ node index.js
Listening on 8000 at /api