A smarter dotenv for Node.js

If you've been coding in Node.js for some time, it's likely that you've used or at least heard of dotenv.

Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env.

It's one of the must-have libraries which I install in nearly all of my projects, until I published typed-dotenv last year.

Demo

Instead of explaining the difference between dotenv and typed-dotenv, let's feel it by seeing how we write my-api-client.js differently.

dotenv

/* my-api-client.js */

const { config } = require('dotenv');
const HttpClient = require('./http-client');

config();

const required = ['MY_API_HOST', 'MY_API_KEY'];
for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing the environment variable "${key}"`);
  }
}

const config = {
  host: process.env.MY_API_HOST,
  apiKey: process.env.MY_API_KEY,
  timeout: parseInt(process.env.MY_API_TIMEOUT) || 5000,
  keepAlive: process.env.MY_API_KEEP_ALIVE === 'true',
};

module.exports = new HttpClient(config);

This is the common way we use dotenv. The code isn't bad right? But can it be better?

typed-dotenv

/* my-api-client.js */

const { config } = require('typed-dotenv');
const HttpClient = require('./http-client');

const { error, env } = config({ rename: { enabled: true } });

// Errors regarding missing required variables, or other config issues.
if (error) {
  throw error;
}

module.exports = new HttpClient(env.myApi);

All in a sudden, the custom validation and data conversion are gone. The code is a lot simpler!

It is basically done for the coding side, but we need one more file - .env.template. This file is for typed-dotenv to do all the hard work, and more importantly, serves as a documentation for others to overview all env-var in one place.

### .env.template ###

##
# @required {string}
MY_API__HOST=

##
# @required {string}
MY_API__API_KEY=

##
# @optional {number} = 5000
MY_API__TIMEOUT=

##
# @optional {boolean} = false
MY_API__KEEP_ALIVE=

Note that the variable names are using double underscores. This is the magic where typed-dotenv turns the variables into the following structure, so you can supply it to new HttpClient(env.myApi) directly.

{
  "myApi": {
    "host": "...",
    "apiKey": "...",
    "timeout": 5000,
    "keepAlive": false
  }
}

Summary

By composing the .env.template file, typed-dotenv can...

  1. convert the env-vars into the desired types (e.g. number, boolean, json, etc.); and
  2. validate if the required env-vars are defined; and
  3. assign default values to the optional env-vars; and
  4. rename the env-vars to fit your purpose; and
  5. document the env-vars in one place; and
  6. ...many more.

If you are interested, please give it a try! Comments are welcome.

GitHub: https://github.com/cytim/nodejs-typed-dotenv
NPM: https://www.npmjs.com/package/typed-dotenv

My Personal Recipe

Last but not least, I found that it's usually helpful to wrap typed-dotenv in a config.js module.

/* config.js */

const { get } = require('lodash');
const { config } = require('typed-dotenv');

const { error, env } = config({
  unknownVariables: 'remove',
  rename: { enabled: true },
});

if (error) {
  throw error;
}

exports.getConfig = (path) => {
  const data = path ? get(env, path) : env;

  if (data === undefined) {
    throw new Error(`The config path does not exist: ${path}`);
  }

  return data;
};

Then you can use it like getConfig('path.to.some.config').

Hope you like it. :)