Photo by Pascal Debrunner on Unsplash
What
If you ever touched backend code you probably already know that environment variables really come in handy when dealing with multiple environments, like local, dev, qa, prod by decoupling configuration from code.
In case you didn't, you may think of environment variables as inputs that you application take as parameters, after all, a program is pretty much like a function, with inputs, outputs and sometimes side effects.
So, just as with functions, where parametrizing values that were previously hardcoded in the function's body yields a more flexible implementation, we may extract hardcoded values from our frontend code as environment variables, so that we are able to change our application behavior without touching the code itself.
Why
When working with a real project you'll probably deal with multiple environments (local, dev, qa, prod) and each of these environments will most likely have its own dedicated API service, and thus each one will be accessed using a different URL.
So instead of hardcoding the API URL, we read this value from an environment variable so that we can deploy the same code for all these different environments.
Another interesting use case for environment variables is to implement feature flags which are used to enable or disable certain features depending on the context (e.g. A/B testing or the application might serve multiple countries/regions and some features might not be available in some of them).
Currently at the place I work we also rely on environment variables to set the "check for updates" polling interval and to tweak some testing scenarios.
In summary, environment variables are a widely supported way of decoupling configuration from code. (See 12factorapp for an in depth explanation)
How
If we were talking about environment variables at the backend we could just npm install dotenv
and dotenv.config()
and then call it a day.
However, as the frontend runs on the client's machine it can't access environment variables (and even if it could, it would make no sense), so we need a different approach.
Enter the compiler
As reading environment variables at run time is not an option for the frontend, we must fallback to compile time substitution.
Nowadays you'll most likely be using a compiler for the frontend, either because you're using JSX, or relying on Babel polyfills, or maybe you recognize the value of static type checking and need to transpile from Typescript.
Even if you don't really care about any of those things, you'll probably be minifying your JS code to reduce the bundle size and get that perfect Page Speed (is this still relevant?) score.
What we're going to do then is use the compiler to substitute environment variables in the code by their actual values at build/compile time.
In this example I'll be using Webpack as it is the standard bundler.
So, supposing you already have your build configuration in place with Webpack, setting up environment variables is a 3-step process:
//webpack.config.js
//1. Read environment variables from our .env file
import dotenv from "dotenv";
dotenv.config();
//2. List environment variables you'll use
// The keys listed here are the ones that will
// be replaced by their actual value in the code.
// Also, their presence will be validated, so that
// if they're undefined webpack will complain and
// refuse to proceed with compilation
const environmentVariables = [
"API_BASE_URL",
"CHECK_FOR_UPDATES_TIME_INTERVAL"
];
//...
//3. Use Webpack's EnvironmentPlugin
plugins: [
//...
new webpack.EnvironmentPlugin(environmentVariables)
//...
],
//...
And then you can use environment variables the same way you'd do with backend code:
const response = await fetch(`${process.env.API_BASE_URL}/login`);
Once again it is very important to keep in mind that what actually happens is essentially textual substitution of environment variables in build time, and a fortunate consequence of this is that for some cases like with feature flags, the minification process is even able to completely wipe out unreachable code branches, eliminating code related to unused features.
By the way, if you ever programmed with C or C++, this substitution process works pretty much the same way the C/C++ preprocessor would when you're using #define
.
Top comments (0)