Welcome to "Javascript: Tricks and tips" In this series of blogs, I'll show you some development tips and will solve some of the problems that I've faced during my career as a Software engineer.
Every time we write a backend application, regardless of its stack, there are configurations that we want to apply to our application: Like tokens, endpoints, keys, etc. There are many reasons we need to avoid “Hard-Coding” (saving those values in our code base). Security (We don’t want to expose our credential information), Dynamic variable (Endpoints, ports, etc..), Scalability(Multiple server and environments), etc…
There are tools out there (paid and free) built for you. But wait for a second! Do we always need a separate application to handle our configurations?. The answer is no.
Environment Variables
An environment variable is a Key-value store hosted by Operating System. Think about it as a javascript variable but defined outside of the program. Most modern operating systems are supporting Environment Variables. Environment Variables are bind to a process. Means you can define a variable called PORT for example for a given process and give it the value of 80 and define PORT for another process and assign 8080. They not gonna step on each other toes.
When you are using Node.js, at runtime, environment variables get load and accessible through “process.env”. Let’s run an example: create a file called app.js
const http = require('http');
http.createServer().listen(8080).on('listening', ()=>{
console.log('listening on port ' + 8080);
});
Run the app.js
node app.js
You would see listening on port 8080 in the console. Now, Let’s use environment variables
const http = require('http');
http.createServer().listen(process.env.PORT).on('listening', ()=>{
console.log('listening on port ' + process.env.PORT);
});
Now you need to run:
PORT=8080 node app.js
Here you are defining a variable called PORT in the scope of app.js and assigning it with value 8080. It’s important to remember, the variable port is only accessible in the process that app.js running.
Now let’s be more creative.
const http = require('http');
const port = (process.env.NODE_ENV === 'prod') ? process.env.PROD_PORT : process.env.DEV_PORT
http.createServer().listen(port).on('listening', ()=>{
console.log('listening on port ' + port);
});
Now we need to define three variables: NODE_ENV, PROD_PORT, DEV_PORT.
If you want to run the application in development mode you should run:
PROD_PORT=3000 NODE_ENV=dev DEV_PORT=8080 node app.js
Result: listening on port 8080
Prod mode:
PROD_PORT=3000 NODE_ENV=prod DEV_PORT=8080 node app.js
Result: listening on port 3000
I guess you already realized what are the problems with this approach. first, When the number of variables increases, It’ll be hard to manage, and It’s error-prone.
Dotenv
Dotenv is a library that loads all of your environment variables from a .env file into your process.env. It is a very convenient way of separating your configuration from your codebase. Now let’s rewrite our application. First, install dotenv:
npm install dotenv
Now we have dotenv, create a file in your root call .env:
NODE_ENV=prod
PROD_PORT=3000
DEV_PORT=8080
We need to load our environment variables using dotenv:
require('dotenv').config();
const http = require('http');
const port = (process.env.NODE_ENV === 'prod') ? process.env.PROD_PORT : process.env.DEV_PORT
http.createServer().listen(port).on('listening', ()=>{
console.log('listening on port ' + port);
});
Now just run app.js! Yea that’s simple! But remember, if you keep any confidential information in your .env file such as passwords, secrets, keys, token, etc… DO NOT PUT YOUR .env FILE IN YOUR CODEBASE. Keep it separate, somewhere safe. Make sure you include .env in your .gitigore file.
My concern right now is what if our deployment goes wrong and for whatever reason, we fail to include our .env at runtime? There are so many different ways to solve that as simple as if statement. But I want to talk about my favorite approach.
Joi
Joi is a very powerful validation tool. It allows you to create complex schemas and validate your object in real time. Let’s create ours: first, install the Joi
Npm install @hapi/joi
Joi has a really simple declarative syntax.
const joi = require('@hapi/joi');
const envSchema = joi.object({
devPort: joi.number().required(),
prodPort: joi.number().required(),
env: joi.string().required(),
});
Now validation is as simple as:
const http = require('http');
const joi = require('@hapi/joi');
require('dotenv').config();
const envSchema = joi.object({
devPort: joi.number().required(),
prodPort: joi.number().required(),
env: joi.string().required(),
});
const environment = envSchema.validate({
devPort: process.env.DEV_PORT,
prodPort: process.env.PROD_PORT,
env: process.env.NODE_ENV,
});
if (environment.error)
throw (environment.error);
const port = (environment.value.env === 'prod') ? environment.value.prodPort : environment.value.devPort;
http.createServer().listen(port).on('listening', ()=>{
console.log('listening on port ' + port);
});
When you declare a Joi schema, it exposes a method called validate which accepts a value and compares it against the schema which you declared. It returns an object with value and error. If no error occurs during the validation process it should be null and you can take the value continue with your code. But if the error is not null, it means validation failed. From here it’s up to you how you want to handle the error. In my case, I want to throw it and kill the process.
You can experiment with the above code snippet by including or excluding some required values in your .env file and see the different results. Now let’s separate our environment from our app.js for the sake of separation of concerns. For start create a file called environment.js:
const joi = require('@hapi/joi');
require('dotenv').config();
const envSchema = joi.object({
devPort: joi.number().required(),
prodPort: joi.number().required(),
env: joi.string().required(),
});
module.exports = envSchema.validate({
devPort: process.env.DEV_PORT,
prodPort: process.env.PROD_PORT,
env: process.env.NODE_ENV,
});
Now Our app.js should look like this:
const http = require('http');
const environment = require('./environment');
if (environment.error)
throw (environment.error);
const port = (environment.value.env === 'prod') ? environment.value.prodPort : environment.value.devPort;
http.createServer().listen(port).on('listening', ()=>{
console.log('listening on port ' + port);
});
Simple isn’t it. Now you have peace of mind. If your deployment breaks your deployment team will notice and will fix it.
Conclusion
Today we covered Environment variables and how we can utilize it to make our applications more secure and scalable. We talked about dotenv and discussed how using that library will make our codebase less error-prone. We also talked about Joi, how to create schemas and how to validate our objects.
I hope you enjoyed my blog today, next time I’ll talk about "How to filter an array based on async function!".
Top comments (2)
Great article... BTW, Cross-env is an alternative too :)
npmjs.com/package/cross-env
You can use it this way:
cross-env NODE_ENV=prd npm run start
thanks for your comment Luiz I'll take a look at that library.