Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Joel Hill
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Joel Hill ( @Jiggidyuo )

Environment Variables Represent Global State And Should Be Accessed By The Application Bootstrapping Logic Only

By on

The more experience that I get with Node.js (ie, JavaScript on the Server), the more I've noticed a rather odd pattern: reusable modules and 3rd-party libraries making reference to Environment Variables. As an industry, we JavaScript developers have generally agreed that "global state" is "not a good thing" - something to be used sparingly and judiciously. And yet, for some reason, environment variables - which represent state that can be accessed globally, by any file - seem to be treated with a blind eye. Now, I have no problem with the use of environment variables as a way to parameterize applications. But, I believe that they should be accessed solely within the bounds of the application bootstrapping logic, at which point they are translated into various constructor arguments and other dependency-injection values.

To paint a concrete picture of what I mean, here's a made-up snippet of code that embodies the pattern that I see a lot:

/* SOME REUSABLE MODULE. */

class MyLib {

	constructor( apiKey = process.env.MY_LIB_API_KEY ) {

		this.apiKey = apiKey;

	}

	inspect() {

		return( `API KEY: ${ this.apiKey }` );

	}

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

/* THE APPLICATION BOOTSTRAPPING PROCESS. */

// Notice that when the application instantiates the MyLib() class, it doesn't provide
// an explicit constructor argument. Instead, it allows the target module to fall-back
// to the GLOBAL STATE represented in the "process.env" object.
var myLib = new MyLib();

console.log( myLib.inspect() );

In this example, the MyLib class is meant to represent some reusable module that will be loaded in more than one application. Notice that when the MyLib class is instantiated, it accepts an "apiKey" argument; but, if that argument is not defined, it will fall-back to pulling data out of the global state (ie, the environment variables).

Now, to its credit, the previous example at least allowed for the apiKey value to be provided to the class constructor. An even more concerning variation on this problem is the module that defines its own Singleton instance without any constructor at all:

const API_KEY = process.env.MY_LIB_API_KEY;

exports.inspect = function() {

	return( `API KEY: ${ API_KEY }` );

};

In this case, the code relies on Node's module resolution caching to, in effect, manage a singleton instance of the "pseudo class" defined within the module file. And, as far as the apiKey value is concerned, the only way to provide it to the module is through the global state (ie, the environment variables).

In order to create a clean separation of concerns (between configuration and behavior), I would recommend isolating the consumption of Environment Variables to the boundary of the application bootstrapping logic, where they can be translated into constructor arguments (or some other form of inversion-of-control mechanic). Going back to the first snippet, this simply means referencing the Environment Variables in the calling context:

/* SOME REUSABLE MODULE. */

class MyLib {

	constructor( apiKey ) {

		this.apiKey = apiKey;

	}

	inspect() {

		return( `API KEY: ${ this.apiKey }` );

	}

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

/* THE APPLICATION BOOTSTRAPPING PROCESS. */

// Notice that when the application instantiates the MyLib() class, it explicitly
// provides the "apiKey" argument. This decouples the MyLib() class form the source of
// the configuration data; and, makes the code much more intuitive and maintainable.
var myLib = new MyLib( process.env.MY_LIB_API_KEY );

console.log( myLib.inspect() );

As you can see, this time, rather than allowing the MyLib class to fall-back to pulling from the global state, we are requiring the "apiKey" argument to be provided by the calling context. This creates a clean line between the functionality of the MyLib class and the configuration of the MyLib class. This clean line makes the code more flexible. And, by removing the "secret dependency" that the MyLib class had on the global state, it makes the code easier to read, maintain, and refactor.

It also makes it very clear which Environment Variables are still in use by the application because they are all referenced in a single place. To those who are just starting a project, this may not sound like an meaningful benefit. But, to those of us that have had to maintain brownfield applications that were written by other people, the at-a-glance understanding of application configuration is tremendously helpful.

Centralizing Environment Variable access also has the beneficial side-effect of making "haphazard module relationships" harder to form. You can't just willy-nilly require one module into another because the consuming context is unlikely to have the necessary configuration values required when instantiating the target module. As such, the very act of isolating Environment Variable access forces the developer to take on a more holistic, more mindful understanding of the entire application architecture.

At the end of the day, I don't have a ton a of Node.js experience. But, much of the Node.js experience that I do have comes in the form of reading and maintaining other people's code. So, while random Environment Variable references may make applications easier to write for the first developer, I can attest that they make applications harder to understand, embrace, and maintain for the next developer. This is why I believe that isolating Environment Variable access within the application bootstrapping process has a host of benefits, including a more flexible application architecture.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel