Simple node.js service configuration

There’s a myriad of npm modules for providing configuration to a node.js application or service, but I’ve always found them simultaneously over-engineered and lacking basic validation of the configuration document.

I’ve settled on a pattern that works very well for my needs. It leverages the excellent Joi library for validation, and simple node built-ins for everything else.

This is the basic pattern, which I put in config.js at the root of the project:

const fs = require('fs');
const Joi = require('@hapi/joi');

const configSchema = Joi.object().required().keys({
    // Attribute schemas go here.
});

module.exports = Joi.attempt(
    JSON.parse(
        process.env.YOUR_APP_CONFIG ||
        fs.readFileSync(process.env.YOUR_APP_CONFIG_FILE || 'config.json', 'utf8')
    ),
    configSchema
);

That’s all there is to it. The module exports the validated config object.

The config document will be read from the first of these that is provided:

  1. The contents of the YOUR_APP_CONFIG environment variable.
  2. The contents of the file whose path is provided in the YOUR_APP_CONFIG_FILE environment variable.
  3. The contents of the file config.json in the working directory.

If the config cannot be read, parsed, or validated, an error will be thrown immediately, preventing the application from starting and providing helpful feedback on what part of the config document is in error.

As an additional advantage, default values specified in the schema will be filled in by Joi. The resulting benefit is that all defaults can be specified in a central location instead of sprinkled throughout the codebase as config.attribute || 'default value' tests.

A great example of this is the service’s listening port. Usually, I will specify this as something like:

const configSchema = Joi.object().required().keys({
    port: Joi.number().integer().min(1).max(65535).default(8080),
});

I never have to check whether config.port is set or has an acceptable value; the schema precisely describes the exact acceptable value domain as well a default value. I am guaranteed that config.port will be set and contain a good value.

Joi is incredibly flexible, and can even make parts of the schema conditional on parts of the input document, specify that some attributes are required only if other attributes are present, specify that exactly one of a set of mutually-exclusive attributes must be specified, and much more. Explore the documentation for more information.

Note that the exported object is mutable. If desired, you can use Object.freeze() to shallow-freeze the config object, or one of the many npm “deep freeze” modules to recursively freeze the entire object graph.

I’ve thought about publishing a module encapsulating this pattern, but after reducing it to its most basic form, there isn’t much left to put in a module. It’s already so short and simple that it’s easier to just copy the pattern.

Leave a Reply

Your email address will not be published. Required fields are marked *