A smarter dotenv for Node.js
Tags: node, javascript, typescript, opensource
Published: 2021-10-18 16:46:23

If you've been coding in Node.js for some time, it's likely that you've used or at least heard of [dotenv](https://github.com/motdotla/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](https://github.com/cytim/nodejs-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

```js
/* 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](https://github.com/cytim/nodejs-typed-dotenv)

```js
/* 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.

```bash
### .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.

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

## Summary

By composing the `.env.template` file, [typed-dotenv](https://github.com/cytim/nodejs-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.

```js
/* 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. :)