Node.js Config the Twelve-Factor Way
I’ve recently been aiming to write all my Node.js applications following the twelve-factor app methodology, a set of principles aimed to help make applications more declarative in their setup, more portable and highly scalable.
While there’s lots of great advice about dependencies, services, disposability and more, there’s one section which caught my attention and made me question how I’d been writing a specific part of my application for a long time, config.
The twelve-factor website defines config as;
An app’s config is everything that is likely to vary between deploys (staging, production, developer environments, etc).
Config is something nearly every application is going to need in some way, shape or form. But how do we best store config? In previous Node.js applications I’ve developed there has often been a config.js file or even a config folder which defines different target configs, e.g. config/production.js.
Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.
Having to amend and possibly commit code to change config isn’t all that sensible of an idea. What’s the alternative?
The twelve-factor app stores config in environment variables (often shortened to env vars or env).
In Node.js you can set environment variables before you run a script or command, for example;
API_ENDPOINT=http://prod-api.com npm start
You can then access this variable in your code with process.env.API_ENDPOINT. This makes it easier to amend values without ever referencing a specific environment in your codebase.
Rather than config files in your codebase you set environment variables at deploy time which can be defined depending on the environment you’re deploying to. This is something platforms like Heroku or now support and the process can be automated with a deploy pipeline such as GitLab CI or Snap CI.
Sometimes apps batch config into named groups (often called “environments”) named after specific deploys, such as the development, test, and production environments in Rails. This method does not scale cleanly: as more deploys of the app are created, new environment names are necessary, such as staging or qa. As the project grows further, developers may add their own special environments like joes-staging, resulting in a combinatorial explosion of config which makes managing deploys of the app very brittle.
This is something I’ve come across and been guilty of in the past, a specific config setup doesn’t quite match what you need so you amend it and create a new one called something like staging-qa. By doing this you’re polluting your codebase and making maintenance more onerous.
The alternative is to not group any config and treat each environment variable as a completely separate value. For example, you need authentication and a username and password adding to your qa and staging environments to ensure only specific people can view your application. Here’s how that could look with environment variables;
SECURED=true USERNAME=user PASSWORD=pass npm start
You can now check process.env.SECURED in your code to see if you need to do an authentication check and can then use the values of process.env.USERNAME and process.env.PASSWORD to validate users against.
If you decide to open up your staging environment then a fresh deploy with SECURED set to false is all that’s needed.
It’s worth noting that environment variables are always converted to strings so be careful when evaluating whether a value is true or false. You can work around this for variables you want to be boolean with code similar to below.
const SECURED = process.env.SECURED === `true`
In production you may not need any authentication which means these environment variables don’t need to be set, you can run npm start. Because process.env.SECURED will evaluate to false the check will not happen and the other two environment variables will not be checked.
The twelve-factor principles have helped me rethink my approach to config. I’ve ended up with a much clearer, scalable and robust solution to configuration in Node.js applications.
Finally, if you’re wanting to validate how twelve-factor your application is, consider this;
A litmus test for whether an app has all config correctly factored out of the code is whether the codebase could be made open source at any moment, without compromising any credentials.