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 intoprocess.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...
- convert the env-vars into the desired types (e.g. number, boolean, json, etc.); and
- validate if the required env-vars are defined; and
- assign default values to the optional env-vars; and
- rename the env-vars to fit your purpose; and
- document the env-vars in one place; and
- ...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. :)