When building any kind of server side app, you will eventually have the following thoughts:
Given values to these kinds of parameters will yield a configuration. It is a crucial part of the system as our application will have lots of configurable elements, so it must be handled properly throughout the codebase.
In this article, we will look at the DOs and DON’Ts of Node config handling.
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);
Better
// 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.
π Read this: Node 8: Five New Features You Need to Knowπππ
Source: Pics me me
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.
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.
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.
Another Node config helping library that we regularly use is Convict. It has multiple benefits over configuration-by-hand approach:
// 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.
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:
dotenv
, convict
) as they will ease the developmentπ Node 8: Five New Features You Need to Know
π Define Mongoose Models Using TypeScript Classes
π How to Build an Ionic 2 App with Native Plugin Integrations