KEEPING IT TOGETHER
Instead of defining parts of the config in separate files, it is strongly advised to have a config.js
file that will hold all configuration in a centralized way.
This will help other developers to locate and adjust config values much more easily, and by having one centralized file ensures reusability of config values and also it can give a quick insight about the project and what technologies/services/libraries are used.
Bad
// db.js
const mongoose = require('mongoose');
const host = 'localhost';
const port = 27017;
const name = 'db';
const connectionString = `mongodb://${host}:${port}/${name}`;
mongoose.connect(connectionString);
// app.js
const express = require('express');
const app = express();
const port = 3000;
app.listen(port);
// config.js
const config = {
app: {
port: 3000
},
db: {
host: 'localhost',
port: 27017,
name: 'db'
}
};
module.exports = config;
// db.js
const mongoose = require('mongoose');
const config = require('./config');
const { db: { host, port, name } } = config;
const connectionString = `mongodb://${host}:${port}/${name}`;
mongoose.connect(connectionString);
// app.js
const express = require('express');
const config = require('./config');
const app = express();
app.listen(config.app.port);
A config file can also export separate variables that hold the values or a simple object with custom depth level so we can also give scope to the config elements.
This is far from perfect as every time we want to change a config value, we have to touch the code. We can overcome this with the help of environment variables.
USE THE ENV LUKE
During testing, we would like to change the database from the one we are using for development. This way we are able to safely drop all data from it and we don’t mess up our development (or production – yikes!) database. Right now we have to edit the config file each time we would like to run tests.
We will make the app aware of the currently set environment. In Node, the NODE_ENV
variable is used for this. Commonly used environments are development or dev when coding on our local machine, production or prod when it is deployed to the live server, test when we are running unit/integration tests (and much more).
The current environment can be retrieved through process.env.NODE_ENV
.
// config.js
const env = process.env.NODE_ENV; // 'dev' or 'test'
const dev = {
app: {
port: 3000
},
db: {
host: 'localhost',
port: 27017,
name: 'db'
}
};
const test = {
app: {
port: 3000
},
db: {
host: 'localhost',
port: 27017,
name: 'test'
}
};
const config = {
dev,
test
};
module.exports = config[env];
Now, when we are running tests (and our NODE_ENV
is set to test), the localhost:27017/test
will be used instead of localhost:27017/db
.
Our app is now aware about its environment; however, when multiple developers are working on a project, their machine setup can (and will) differ from each other and also from the server they are deploying the app.
Environment variables are the go-to solution for this kind of problem too. Aside from creating environment specific configs, we should also offer more fine-grained options too. This means that each element of the configuration should be manipulated through its own environment variable, but a default value should be provided that works for most cases.
// config.js
const env = process.env.NODE_ENV; // 'dev' or 'test'
const dev = {
app: {
port: parseInt(process.env.DEV_APP_PORT) || 3000
},
db: {
host: process.env.DEV_DB_HOST || 'localhost',
port: parseInt(process.env.DEV_DB_PORT) || 27017,
name: process.env.DEV_DB_NAME || 'db'
}
};
const test = {
app: {
port: parseInt(process.env.TEST_APP_PORT) || 3000
},
db: {
host: process.env.TEST_DB_HOST || 'localhost',
port: parseInt(process.env.TEST_DB_PORT) || 27017,
name: process.env.TEST_DB_NAME || 'test'
}
};
const config = {
dev,
test
};
module.exports = config[env];
For example, when John runs the tests, his Mongo DB runs in Docker on port 27000
instead of the default 27017
. By explicitly setting TEST_DB_PORT
to 27000
, John can easily set the system to his preferences without code modification, and for others connecting to Mongo DB will still work out of the box. Environment variables are also supported by most CIs and PaaS providers so your Node config can also be easily set when your code is verified on the build server, or it is deployed to your production environment.
One quick note here that every variable read from process.env
will be a String
; thus, type conversion is needed sometimes (parseInt
for converting to Number
).
We can adjust the system’s individual configurations without code modification. That looks good; however, we need to talk about sensitive configurations.
KEEP YOUR SECRETS TO YOURSELF
All these values stored in config.js
will be available in the code’s repository. Putting the default port on which the app will bind into the code is not really an issue; however, the login credentials for the production database or the API key for that cool third party service is a big no-no. Both adding and removing them :).
The ideal way to handle these situations is to leave the sensitive configuration variables blank in the code, then set the environment variables on the system where your app is running.
// config.js
const config = {
coolThirdPartyApiKey: process.env.COOL_THIRD_PARTY_API_KEY
};
module.exports = config;
// in the terminal
COOL_THIRD_PARTY_API_KEY=supersecret&node [entry-point].js
Of course, when running the app on your own machine or deploying it to your own server (or IaaS), every sensitive environment variable must be set every time the app is started. This can be tedious; however, there are great tools which can help you overcome this problem.
DO THE DOTENV
Dotenv is a great Node library that enables the use of a .env
file that can hold environment variables and feed them to your Node app.
// .gitignore
.env
// .env
COOL_THIRD_PARTY_API_KEY=supersecret
// config.js
require('dotenv').config(); // this loads the defined variables from .env
const config = {
coolThirdPartyApiKey: process.env.COOL_THIRD_PARTY_API_KEY
};
module.exports = config;
Just make sure (as stated in the example) to add .env
to the .gitignore
file or else you will commit the sensitive variables to the repository (which is again, unwanted).
Aside from dotenv
, there is direnv
, but we won’t cover it in this article, but it’s worth checking out.
CONVICT, MORE LIKE PRO-VICT (SORRY)
Another Node config helping library that we regularly use is Convict. It has multiple benefits over configuration-by-hand approach:
- We can define a configuration schema with type validation
- Config variables can have custom validations (port, IP, or plug your own validator)
- We can define an environment variable to the config property so it will be env aware
- We can define a switch to the property so it can be configured in a command line argument
- The config value coming from the environment variable will be automatically cast to the desired type
- And much more…
// config.js
require('dotenv').config();
const convict = require('convict');
const config = convict({
env: {
format: ['prod', 'dev', 'test'],
default: 'dev',
arg: 'nodeEnv',
env: 'NODE_ENV'
},
apple: {
format: String,
default: 'from default',
arg: 'apple',
env: 'APPLE'
},
banana: {
format: String,
default: 'from default',
arg: 'banana',
env: 'BANANA'
},
cherry: {
format: String,
default: 'from default',
arg: 'cherry',
env: 'CHERRY'
}
});
const env = config.get('env');
config.loadFile(`./config/${env}.json`);
config.validate({ allowed: 'strict' }); // throws error if config does not conform to schema
module.exports = config.getProperties(); // so we can operate with a plain old JavaScript object and abstract away convict (optional)
// config/prod.json
{
"banana": "from prod.json"
}
// .env
NODE_ENV=prod
// from terminal
node index.js --cherry 'from command line'
// index.js
const config = require('./config');
console.log(config);
/**
{
env: 'prod',
apple: 'from default',
banana: 'from prod.json',
cherry: 'from command line'
}
*/
In this (rather complex) example we can take a glimpse of what convict
combined with dotenv
can give us.
CONCLUSION
In this article we’ve given a step-by-step guide of how to build an efficient configuration module for your Node app. The principles can also be applied to other technologies as well. The key takeaways from this post are the following:
- Centralize your app’s configuration to a single module
- Enable config variable setting without any code modification
- Use the environment variables
- Do not expose secret configuration values
- Don’t be afraid to use third-party config libraries (
dotenv
,convict
) as they will ease the development
Top and Reliable Node.js Hosting Recommendation Discount 15% Off
ASPHostPortal as top and reliable Node.js hosting provider. ASPHostPortal.com is Microsoft No #1 Recommended Windows and ASP.NET Spotlight Hosting Partner in the United States. Microsoft presents this award to ASPHostPortal for the ability to support the latest Microsoft and ASP.NET technology. Use ASPHostPortal.com’s website building tools to get that special, customized look for your website. A nifty wizard will walk you through the process. Other companies promise cheap hosting, but then charge extra for setup fees, higher renewal rates, or promotional services. With ASPHostPortal.com, the listed price is the number you’ll pay, and you can expect a fully loaded, comprehensive suite of web services. Their powerful servers are especially optimized and ensure the best Node.js performance. They have the best data centers on three continent and unique account isolation for security. ASPHostPortal.coweb spacece explorer lets you manage your website files with a browser. A control panel lets you set up and control your server functions with ease.