How to properly use Environment Variables

Environment variables is one of the basic concepts for application developers. They are something we use every day. Environmental variables have many advantages, including program configuration and security.

Environmental variables are excellent. However, everything is costly, and these variables, if used carelessly, can have detrimental effects on our code and programs.
Environmental variables
If environmental variables help us write more secure code and configure programs more easily in different environments, how can they be a bad thing?
Environmental variables are global and external, through which application developers are able to inject configurations and manage them where they are more difficult to adapt.
We all know as developers that global situations can be bad for our applications. In this article, we will focus on the two main drawbacks that we encounter most when dealing with environmental variables:
Poor inflexibility / testability
Readability / Comprehension of code
How to use environmental variables correctly
Similarly, how we deal with global variables or global patterns (such as singleton) that are applied in bad places is our favorite weapon of injecting dependency.
It's not supposed to be exactly what we do for code dependencies, but the principles are the same. Instead of using environmental variables (dependencies) directly, we inject them into places that are actually used (callsites). Reverse this relationship from "dependent callsites" to "required callsites".
Dependency injection solves these problems using the following:
Allows developers to easily inject configurations during testing.
So how do we apply these principles?
We use a Node.js example to see how we can refract a base code and eliminate individual environmental variables.
Let's say we have a simple program with an endpoint that queries for all TODOs in a PostGres database. Here is our database module with separate environmental variables:

  1. const { Client } = require("pg");  
  3. function Postgres() {  
  4.   const c = new Client({  
  5.     connectionString: process.env.POSTGRES_CONNECTION_URL,  
  6.   });  
  7.   this.client = c;  
  8.   return this;  
  9. }  
  11. Postgres.prototype.init = async function () {  
  12.   await c.connect();  
  13.   return this;  
  14. };  
  16. Postgres.prototype.getTodos = async function () {  
  17.   return this.client.query("SELECT * FROM todos");  
  18. };  
  20. module.exports = Postgres;  

And this module is injected into our HTTP controller through the program input:

  1. const express = require("express");  
  2. const TodoController = require("./controller/todo");  
  3. const Postgres = require("./pg");  
  5. const app = express();  
  7. (async function () {  
  8.   const db = new Postgres();  
  9.   await db.init();  
  10.   const controller = new TodoController(db);  
  11.   controller.install(app);  
  13.   app.listen(process.env.PORT, (err) => {  
  14.     if (err) console.error(err);  
  15.     console.log(`UP AND RUNNING @ ${process.env.PORT}`);  
  16.   });  
  17. })();  

Looking at the entrypoint file above, we have no way of defining the program needs for environmental variables (generally configuring the environment globally).
Refractory code
The first step in improving the code already specified is to identify all locations that use environmental variables directly.
For our special cases above, it is very straightforward because the base code is small. But for larger code, you can use tools like eslint to scan all locations that use environment variables directly. Just set a rule, such as banning environment variables (like node / no-process-env from eslint-plugin-node).

Now it's time to eliminate the direct use of environmental variables from our application modules and consider these configurations as part of the module requirements:

  1. ...  
  2. function Postgres(opts) {  
  3.   const { connectionString } = opts;  
  4.   const c = new Client({  
  5.     connectionString,  
  6.   });  
  7.   this.client = c;  
  8.   return this;  
  9. }  
  10. ...  

These configurations are generated only from the point of entry of our application:

  1. ...  
  2. const db = new Postgres({  
  3.   connectionString: process.env.POSTGRES_CONNECTION_URL,  
  4. });  
  5. ...  

Given the point of entry, it is quite clear what the environmental conditions of our program are now. This prevents potential problems with environmental variables that are forgotten to be added.

But you may have a question here:
Why not use a central module / file?
We have seen several solutions to this problem using a central location to plot these values ​​(such as the module / config.js file for Node projects).
But this method is no better than using the environment variables provided by the program runtime (like process.env) because everything is still somewhat global (a configuration object used throughout the program).
In fact, it could be even worse because now we are introducing another place for the code.

In this article, we tried to show you with a simple example how to use environmental variables correctly. Always keep in mind that misuse of these variables can have consequences for your program


Working with environmental variables What is Environment Variables Correct use of Environment Variables Incorrect use of environmental variables Application of environmental variables Correct use of Environment Variables in the program
You must be logged in to post a comment