Environment variables in Webpack builds vs. Node.js apps

I recently had a discussion with an off-shore team mate who was supposed to add a new environment variable to a client-side app and re-deploy it. He had some trouble doing this, and after talking to him and illustrating how frontend apps use env vars in contrast with Node.js APIs, he finally understood the nuances

His misunderstanding was about the way Webpack manages environment variables. Being an ops person, he assumed that environment variables work the same way they do for our Node.js services.

Namely, he thought that whenever we add / remove / change an environment variable used by our frontend app (Vue.js), these changes are automatically picked up by “restarting” the app — in our case, I believe he thought about restarting NGINX.

There is a difference between the way Node.js handles environment variable updates, compared to the way Webpack does it. Although Webpack is a Node.js-based tool, and it makes heavy use of it, its output is plain old HTML, CSS, JavaScript and maybe assets such as fonts, images and other media. There is no way for it to parse environment variables at runtime.

This means environment variables are processed at compile-time rather than startup/runtime.

Node.js apps can pick up environment variable changes by simply restarting them. If you use something like PM2 to keep your apps running, you type pm2 reload <APP_NAME> and you’re good to go. New env vars are available.

It’s different with a Vue.js app, for example, or even a React.js app for that matter. Any app that uses Webpack as its main build tool falls into the same category and faces the same shortcomings.

I’ll try to do a naive explanation of the process Webpack uses, to handle environment variables and other “static” values.

Whenever it has to build the application code, Webpack looks at references to variables whose values don’t change during the application lifecycle. By generating the AST (Abstract Syntax Tree), Webpack is able to look at your code the same way the JavaScript Tokenizer(Lexer) - Parser - CodeGen trio does it — see Kyle Simpson’s explanation from You-Dont-Know-JS/ scope & closures /ch1.md.

This means that it’s able to tell, whether, during the execution time, the value of a specific variable gets changed, if the variable is reassigned or some of its properties are changed, in the case of reference types.

When it encounters something like: const cacheKeyPrefix = 'myAppCache'; it will notice that this variable never changes throughout the execution of the code. It knows it because it is a constant and because a primitive value is assigned to it — reference types can be overwritten partially, even if they’re assigned to constants.

After establishing that the value is static, it will replace every usage of that variable with the actual string. So wherever it finds cacheKeyPrefix used, it will replace it with the actual value assigned to the variable — myAppCache.

Here’s how such a snippet would look before and after Webpack processes it:

// Before Webpack
const cacheKeyPrefix = 'myAppCache';

// Somewhere in a different file
const cachedUserData = cache[`${cacheKeyPrefix}_userData`];

// After Webpack
var cacheKeyPrefix = 'myAppCache';
var cachedUserData = cache['myAppCache_userData'];

I’m actually not sure if it keeps the initial variable declaration or if it removes it completely, but it serves to drive the point home.

On the same token, to circle back to environment variables, whenever someone uses: process.env.SOME_VAR_NAME in their client-side code, upon building the app, Webpack extracts the value of that environment variable and replaces every occurrence of it, in the code, with the actual value.

Even if they don’t explicitly use process.env in the code, but instead do a variable assignment like const someVar = process.env.SOME_VAR_NAME; Webpack can apply the same treatment and get to the same result.

Webpack can do this because while building the app, it has full access to the Node.js runtime so it is able to use the FileSystem API, environment variables and anything else Node.js has to offer.

I believe this is also the way it can figure out the proper paths for dynamic imports and what it basically does after figuring them out is to replace the dynamic paths with actual, “absolute” paths.

To conclude, in front-end web applications, environment variables are bundled directly with the code, at compile-time, by Webpack.

By contrast, with Node.js applications / APIs, environment variables can be “refreshed” by restarting the app and allowing it to pick up the new variables and values.

As one of my co-workers put it: environment variables in frontend-apps build with Webpack are more like build parameters, since they’re parsed and interpreted only at build time and remain unchanged during runtime.

Keep an open mind!

Image credits: By Thaddeusw GFDL, CC-BY-SA-3.0 or FAL, from Wikimedia Commons

Copyright (c) 2023 Adrian Oprea. All rights reserved.