Environment variables allows us to customize the behavior of our application in different environments. Typically most of the applications have three basic environments that they operate in:
- local/development
- staging/pre-production
- production
The good part is we don’t change our source code explicitly to make it work in a particular environment but take some values into considerations that these environments provide as configs and read them into our source code basis which the behavior of our application changes.
Accessing Environment Variables in JavaScript
Usually all the JS projects exposes all the environment variables available during process execution on process.env
object. For example your static asset’s path could be different in different environment i.e for local it could be
1localhost:8888/assets/images/logo.png
for staging it could be
1https://myapp.stage.in/assets/images/logo.png
and for the production it could be
1https://myapp.in/assets/images/logo.png
which means the only part changes is the hostname so to account for this usually we pass the hostname as process.env
variable
1process.env.HOST_NAME=https://myapp.stage.in
and in our application we’ll use it like
1<img src=`${process.env.HOST_NAME}/assets/images/logo.png` alt="website logo"/>
To set the environment variables on process.env
object you can use dotenv
That’s it we can now deploy the same source code in a different environment and the application will fetch image based on the HOST_NAME
provided by the respective environments.
Environment variables are very powerful and it can be considered as configurations that can vary based on where we are deploying our application and the behavior of our application is decided based on these configurations. Today we build our applications in a way that they can be either built as static applications or dynamic applications similarly the environment variables can also be categorized into two:
- Static Environment Variables
- Dynamic Environment Variables
Let’s talk about each one of them
Static Environment Variables
Static Environment Variables are variables that are available during the build time of your application and are replaced with their actual values during build time which makes it impossible to change the value at runtime. To change the values you need to rebuild your application and deploy it again.
Now take a moment and think about what I just said. I just said above that we can use process.env.KEY
which means they can be changed at runtime as well, right? Then what do I mean by static environments, what is all this confusion? 🤔
There are few things which are available during build time only and never tend to change at runtime. For example
1PORT=88882CDN_PATH_PREFIX=https://mycdn.com3SENTRY_KEY=123basdb123124ASSETS_PATH=/static5STAGE=staging6# and the list goes one
So now since we know that these values don’t tend to change and to change them it should be cautiously changed and the application should be re-built and deployed. Hence, it’s not a good practice to bundle the whole process.env
object for this since we know exactly what values we want and bundling the whole process.env
increases your bundle size drastically. What do we do to fix this?
All the modern bundling tools like webpack, rollup allows you to overcome this issue through some plugins. Let’s see examples
Webpack
Webpack provides a plugin called DefinePlugin
that allows you to create global constants which can be configured at build time. Here’s how it can be done
1new webpack.DefinePlugin({2 PORT: `'${process.env.PORT}'`,3 CDN_PATH_PREFIX: `'${process.env.CDN_PATH_PREFIX}'`,4 SENTRY_KEY: `'${process.env.SENTRY_KEY}'`,5 ASSETS_PATH: `'${process.env.ASSETS_PATH}'`,6 STAGE: `'${process.env.STAGE}'`,7});
So when you build your application webpack will evaluate all the process.env
values on the right and assign them to the keys variable on the left. So now in your application anywhere you can use these globals directly.
1<img src=`${CDN_PATH_PREFIX}/${ASSETS_PATH}/images/logo.png` alt="website logo"/>
Rollup
Rollup also provides a similar plugin called replace
which does the same thing as above i.e replace all the environment variables with their actual values at build time.
1import replace from '@rollup/plugin-replace';23export default {4 input: 'src/index.js',5 output: {6 dir: 'output',7 format: 'esm'8 },9 plugins: [replace({10 PORT: `'${process.env.PORT}'`,11 CDN_PATH_PREFIX: `'${process.env.CDN_PATH_PREFIX}'`,12 SENTRY_KEY: `'${process.env.SENTRY_KEY}'`,13 ASSETS_PATH: `'${process.env.ASSETS_PATH}'`,14 STAGE: `'${process.env.STAGE}'`,15 })]16};
So now in your application anywhere you can use these globals directly.
1<img src=`${CDN_PATH_PREFIX}/${ASSETS_PATH}/images/logo.png` alt="website logo"/>
Static variables works perfect for most of the use cases where we know the values of the environment variables upfront at build time but there are still few things which are very dynamic in nature and can’t be evaluated at build time for example the key to communicate with your backend API server. The key can be changed(a.k.a key rotation) by your infrastructure team at any point in time but that doesn’t mean you should rebuild your application everytime the key changes. So these types of variables can’t be statically analyzed by webpack/rollup and used in your application. So how do we solve this? Dynamic Environment Variables 🎉
Dynamic Environment Variables
Dynamic environment variables are the ones who’s values keeps on changing and any change in them shouldn’t cause our application to be re-built and re-deployed which means we can’t use webpack’s DefinePlugin
or rollup’s replace-plugin
. So how do we manage to access environment variables at runtime? 🤔
Before answering this question let’s first identify what are different types or different nature of application we can have
- Static web application
- Server rendered web application
- Node application
Let’s see how we can access dynamic environment variables in each of these application types
1. Static web application
Static web applications are generally applications that can just be built with gatsby/next/create-react-app/your custom setup and hosted on netlify, Zeit(now vercel) or s3(a.k.a JAMstack sites). These applications generally loads and then can get some dynamic data by making some XMLHttpRequest calls via fetch
but they don’t really need servers to render their web pages.
So which means since you can’t have servers then how can you have dynamic runtime variables in your applications? Here’s how can you do that
1<!-- index.html -->2<html>3 <head>4 <!-- get your environment values -->5 <script src="https://myserver.com/environment.js"></script>6 <title>My static web app</title>7 </head>8 <body>9 <div id="root">10 <!-- You app gets mounted here-->11 </div>12 </body>13</html>
The script tag above makes a request to fetch the variables to your server. Assume you have an express server running on https://myserver.com
. Here’s how you can make your server fulfil this request
1// server.js2app.use('/environment.js', (req, res) => {3 const API_HOST = process.env.API_HOST4 res.setHeader('Content-Type', 'text/javascript');5 res.send(`window.API_HOST = ${API_HOST}`);6});
Now in your application you can easily refer API_HOST
by window.API_HOST
and everytime your static application is visited or refreshed the value of API_HOST
is always up to date 🥁
2. Server rendered web application
With Server rendered applications things are comparatively simple since every request hits the server that then generates the markup and sends back to the client(browser in this case). Let’s see how we can access a dynamic environment variable in this case.
Assuming we have express server doing the server-side rendering. Here’s how the setup will look like
1// server.js2app.use('*', (req, res) => {3 const API_HOST = process.env.API_HOST;4 res.setHeader('Content-Type','text/html');5 res.send(6 `<html>7 <head>8 <script>window.API_HOST=${API_HOST}</script>9 <title>dummy express app</title>10 </head>11 <body>12 <div id="root">13 <!-- Your application mounts here -->14 </div>15 </body>16 </html>`17 ),18}
Now in your application you can easily refer API_HOST
by window.API_HOST
. Since our html markup is being generated by our server we evaluate the environment value on every request and then add it to window object inside a script tag which will be executed in the browser and set the API_HOST
on the window
scope
1<script>window.API_HOST=${API_HOST}</script>
3. Node application
Accessing dynamic environment variables in a node application is the simplest of all. Since it’s a process constantly running you always can refer the process.env.VARIABLE_NAME
and expect to get the actual up to date value of a particular variable set in that environment at that point of time.
1// server.js2app.use('/get-data', async (req, res) => {3 const API_HOST = process.env.API_HOST4 const response = await fetch(`${API_HOST}/some-downstream-service-endpoint`)5 res.send({ data:response.data });6});
If you want to make it more modular then there’s another appraoch as well. With this approach you can fine tune what you exactly want to export and can also write some custom logic around it.
1// environment.js2const getEnvironment = () => {3 if (process.env.STAGE === 'development' || process.env.STAGE === 'test') {4 //import .env in development and for all other environments expect ENV variables to be made available by infrastructure team as part of OS env variables5 require('dotenv').config({ path: '.env.development' });6 }7 return {8 apiHost: process.env.API_HOST,9 authenticationUrl: process.env.AUTHENTICATION_URL,10 };11}12export default getEnvironment1314// server.js15import getEnvironment from './environment.js'16const { apiHost } = getEnvironment();1718app.use('/get-data', async (req, res) => {19 const response = await fetch(`${apiHost}/some-downstream-service-endpoint`)20 res.send({ data:response.data });21});
Wrap Up 📝
- Environment variables are an important part of every application assume it’s like a configuration for your application that helps it run in different environments without developer touching the source code.
- You need to always cautiously choose between what keys are static and what keys are dynamic.
- Always use webpack’s
DefinePlugin
and rollup’sreplace
plugin if your environment variables are static. - Don’t use webpack’s
DefinePlugin
and rollup’sreplace
plugin if your environment variables are dynamic since these plugins evaluate the value at build time and not at runtime of your application. - You can use
dotenv
for making your environment variables exposed on theprocess.env
object. - Never ever commit your
.env
files to version control like git
Phew! That was a wild ride ⚡️. I hope you learned something out of this.
If you have some thoughts around handling environments or want to share your experiences of setting environments for your company or side projects then you can write it to me or you can DM me on Twitter. I would love to hear them!
P.S. If you like this, make sure to subscribe, share this with your friends and follow me on twitter 😀