Improve Code Repository Quality by following this coding pattern!

tl;dr: Check out the code repo at https://github.com/teamzerolabs/config-service-reference.

Does your project connect to Databases or 3rd party Apis?

Most likely yes. As long as your program is connecting or making calls to remote endpoints, it needs to get the security credentials from somewhere.

There are many different ways to load these credentials into the program:

This is common in GitHub starter projects, let’s hope we can educate and reduce this.

Level 1: Hard code it in the text file itself — This usually happen on a Friday, or for people just starting out. The reference repo should help you have code that you can pull in to use instead of doing this.

Level 2: Loading it from the process.env object in the place where the connection happens — This is better than hard coding, but over time makes the environment variable a little hard to track. Because you cannot go to a single file to find all of the referenced environment variables.

Level 3: Load all of the environment configurations from a single file — This is provided in this article and code example! This gives you the benefit of knowing where to go to look up configurations, and allow the program to fail early if bad values were supplied.

Level 4: Turn the configuration script into a service — By doing so, we gain the ability to do other interesting things: type-checking loaded values, and loading additional configurations from S3 or database, before other initialization code runs.

Before you start

Have a Node.JS runtime ready (Version > 10)

Get Postman if you haven’t — Alternatively, you can use the browser or curl to hit the Api.

Check out https://github.com/teamzerolabs/config-service-reference into your local space.

Have a MySQL up and running: I have included a docker-compose file in the db-setup folder, you can spin up by going there and running docker-compose up -d .

A Quick Example — Minimum Book Service that returns books stored in MySQL database

3 good books!

In the first folder node-starthere , we have two files:

main.js — This is where we setup the express server to serve the request at localhost:3000/books

server to serve the request at models/index.js — We will connect into MySQL with mysql2 and sequelize .

and . Run yarn start to get started.

You can see how the database credentials are currently stored in models/index.js :

It is hard coded here, but we can do better.

The Downside of hard coding Database credentials

It is not secure — Anyone checking out the public repository will now know too much, especially if your database instance is public, it is now exposed.

It is difficult to work with different environments — If you need to connect this code to testing or production deployments, you don’t want to do code changes just for deployment.

Step#1 — Load each of these from environment variables

You can read more about environment variables here. The gist of it is that you can pass values into the running program like this

DB_NAME=configexample DB_HOST=localhost DB_USER=root DB_PASSWORD=dIKnUfyfUPURi9irSplTOqGO4OtE0 DB_PORT=3306 yarn start .... // In the program

const sequelize = new Sequelize(

process.env.DB_NAME,

process.env.DB_USER,

process.env.DB_PASSWORD,

{

dialect: "mysql",

host: process.env.DB_HOST,

port: parseInt(process.env.DB_PORT)

}

);

This approach is fine if you have a small number of configurations, but as we all know any projects that survived the first month of usage will start to integrate with more services. (A recent 4 month project of mine has 40 variables! Imagine typing them out for the yarn start statement above, it’s too much).

Step#2 — Load Environment variables at runtime for local development before use.

Luckily there is a package that will save us from typing out the variables over and over. It is called dotenv . You use dotenv like so:

const dotenv = require("dotenv"); let env = process.env.NODE_ENV;

if (env !== "production") {

// Development will use .env.

// Anything else will use .env.<NODE_ENV>

if (env === "development") {

env = "";

}



const path = `src/config/.env${env ? `.${env}` : ""}`;

// Well, it's better to use winston logger, perhaps we can cover in another tutorial

console.log(`Loading config from environment file ${path}`);

dotenv.config({ path });

}

Put your code under src, and make a config folder, and create a .env file inside there:

DATABASE_URL=localhost

DATABASE_PORT=3306

DATABASE_USERNAME=root

DATABASE_PASSWORD=dIKnUfyfUPURi9irSplTOqGO4OtE0

DATABASE_NAME=configexample

And dotenv.config will load the variables in the text files into process.env , now you can skip typing out the variables at yarn start !

Step#3 — Convenience methods to parse values and exit early if they are missing

The only thing more dangerous than a misconfigured program is a misconfigured program running for months on end.

If we forgot to make the .env file, or put bad values into it, ideally we want to know as early as possible. Running code with incomplete or wrong configuration makes troubleshooting difficult.

If you look at the second folder node-with-config-service , you can see that the following utility methods are added and used to parse the variables:

/**

* Retrieves environment variable and throws if missing without default

*

* @param name name of environment variable

* @param defaultVal

*/

function parseEnv(name, defaultVal) {

let env = process.env[name];

if (!env) {

if (isNullOrUndefined(defaultVal)) {

throw new Error(`Missing environment variable for ${name}`);

}

env = defaultVal;

}



return env;

}



/**

* Retrieves environment variable as a number and throws if missing without default

*

* @param name name of environment variable

* @param defaultVal

*/

function parseEnvNumber(name, defaultVal) {

const number = parseInt(parseEnv(name, `${defaultVal}`));

if (isNaN(number)) {

throw new Error(`Bad environment variable for ${name}: Not a Number`);

}

return number;

}



/**

* Retrieves environment variable as a boolean and throws if missing without default

*

* @param name name of environment variable

* @param defaultVal

*/

function parseEnvBoolean(name, defaultVal) {

return parseEnv(name, `${defaultVal}`).toLowerCase() === "true";

}

And we rewrite the database connection section to this:

// in src/config/index.js const config = {

app: {

port: parseEnvNumber("APP_PORT", 3000)

},

mysql: {

host: parseEnv("DATABASE_URL"),

port: parseEnvNumber("DATABASE_PORT", 3306),

username: parseEnv("DATABASE_USERNAME"),

password: parseEnv("DATABASE_PASSWORD"),

database: parseEnv("DATABASE_NAME")

}

}; // in src/models/index.js const config = require("../config");



const sequelize = new Sequelize(

config.mysql.database,

config.mysql.username,

config.mysql.password,

{

dialect: "mysql",

host: config.mysql.host,

port: config.mysql.port

}

);

The program will now exit early with errors if environment variables are missing, and you can depend on a static configuration object existing from anywhere in the program!

There are two more extensions to this approach — You can apply the same patterns in TypeScript, and go a step beyond by setting up an actual helper service in NestJS. Both are included in the git repo. We will cover these in a future TypeScript+NestJs article!