Taming web application configurations using node-convict

Original author: Zachary Carter
  • Transfer
  • Tutorial
From a translator: This is the seventh article in the Node.js series from the Mozilla Identity team, which is involved in the Persona project .





In this article in the Node.js series, we will look at the node-convict module, which helps you manage your Node.js application configurations. It provides transparent default settings and built-in typing to make it easier to find and correct errors.

Formulation of the problem


There are two main problems that make it necessary to configure applications:

  • Most applications can run in multiple environments with different configuration options.
  • Including credentials and other confidential information in the application code can cause problems.

These problems can be solved by initializing some variables depending on the current environment and using environment variables to store sensitive data. The template generally accepted in Node.js environment for implementing this approach consists in creating a module that exports the configuration:

var conf = {
  // окружение приложения - 
  // "production", "development", или "test
  env: process.env.NODE_ENV || "development",
  // IP адрес
  ip: process.env.IP_ADDRESS || "127.0.0.1",
  // Порт
  port: process.env.PORT || 0,
  // Настройки БД
  database: {
    host: process.env.DB_HOST || "localhost:8091"
  }
};
module.exports = conf;

This works fine, but there are a couple more problems:

  • What if the configuration contains incorrect data? We can save time and nerves by detecting errors as early as possible.
  • How easy is it for administrators, testers and other members of a large team to understand the configuration when they need to change settings or look for defects? A more declarative and better documented format would make their life easier.


Introducing convict


node-convict solves both of these problems by providing a configuration scheme where you can specify type information, default values, environment variables, and documentation for each setting.

Using convict, the example above takes the form:

var conf = convict({
  env: {
    doc: "The applicaton environment.",
    format: ["production", "development", "test"],
    default: "development",
    env: "NODE_ENV"
  },
  ip: {
    doc: "The IP address to bind.",
    format: "ipaddress",
    default: "127.0.0.1",
    env: "IP_ADDRESS"
  },
  port: {
    doc: "The port to bind.",
    format: "port",
    default: 0,
    env: "PORT"
  },
  database: {
    host: {
      default: "localhost:8091",
      env: "DB_HOST"
    }
  }
});
conf.validate();
module.exports = conf;

It contains almost the same information, but presented in the form of a diagram. Thanks to this, it is more convenient for us to export it and display it in a readable form, to do validation. The declarative format makes the application more reliable and more friendly to all team members.

How the circuit works


There are four properties for each setting, each of which helps make the application more reliable and easier to understand:

  1. Type . The property formatis specified, or one of the built-in types convict ( ipaddress, port, intetc.) or a function to validate the user types. If during validation the parameter does not pass the type check, an error occurs.
  2. The default values . Each parameter must have a default value.
  3. Environment variables . If the variable specified in is env,set, then its value will be used instead of the default value.
  4. Documentation . The property docis quite obvious. The advantage of including documentation in the diagram over comments in the code is that this information is used in the method conf.toSchemaString()for more informative output.

Additional configuration levels


Over the foundation of the default values, you can add additional levels of configuration using calls conf.load()and conf.loadFile(). For example, you can load additional parameters from a JavaScript object for a specific environment:

var conf = convict({
  // схема та же, что и в предыдущем примере
});
if (conf.get('env') === 'production') {
  // в боевом окружении используем другой порт и сервер БД
  conf.load({
    port: 8080,
    database: {
      host: "ec2-117-21-174-242.compute-1.amazonaws.com:8091"
    }
  });
}
conf.validate();
module.exports = conf;

Or you can create separate configuration files for each of the environments, and load them with conf.loadFile():

conf.loadFile('./config/' + conf.get('env') + '.json');

loadFile() It can also upload multiple files at once, if you pass an array of arguments:

// CONFIG_FILES=/path/to/production.json,/path/to/secrets.json,/path/to/sitespecific.json
conf.loadFile(process.env.CONFIG_FILES.split(','));

Download additional parameters through load()and loadFile()is useful when there is a setting for each of the environments, which should not be set in the environment variables. Separate declarative configuration files in JSON format allow you to more clearly represent the differences between the parameters in different environments. And since files are uploaded using cjson , they can contain comments, which makes them even more understandable.

Please note that environment variables have the highest priority, higher than the default settings and settings loaded through load()and loadFile(). To check which settings are effective, you can call conf.toString().

“V” means validation


After the settings are loaded, you can run the validation to check if they all have the correct format in accordance with the scheme. There are several built-in formats in convict, such as url, portsor ipaddressyou can also use built-in JavaScript constructors (for example Number). If the property is formatnot set, convict will check the parameter type to match the default type (by calling Object.prototype.toString.call ). The three schemes below are equivalent:

var conf1 = convict({
    name: {
      format: String
      default: 'Brendan'
    }
  });
// если формат не указан, предполагаем, что тип должен быть
// такой же, как у значения по умолчанию
var conf2 = convict({
    name: {
      default: 'Brendan'
    }
  });
// более лаконичная версия
var conf3 = convict({
    name: 'Brendan'
  });

The format can also be specified as an enumeration, explicitly specifying a list of valid values, for example ["production", "development", "test"]. Any value that is not in the list will not pass validation.

Instead of built-in types, you can use your own validators. For example, we want the parameter to be a string of 64 hexadecimal digits:

var check = require('validator').check;
var conf = convict({
    key: {
      doc: "API key",
      format: function (val) {
        check(val, 'should be a 64 character hex key').regex(/^[a-fA-F0-9]{64}$/);
      },
      default: '3cec609c9bc601c047af917a544645c50caf8cd606806b4e0a23312441014deb'
    }
  });

The call will conf.validate()return detailed information about each erroneous setting, if any. This helps to avoid redeploying the application when each configuration error is detected. This is what the error message will look like if we try to set the parameter keyfrom the previous example to 'foo':

conf.set('key', 'foo');
conf.validate();
// Error: key: should be a 64 character hex key: value was "foo"

Conclusion


node-convict extends the standard Node.js application configuration template, making it more reliable and convenient for team members who do not have to understand the wilds of imperative code to check or change settings. The configuration scheme gives the project team more context for each setting and allows you to do validation for early detection of errors in the configuration.




Also popular now: