logo
Navigate back to the homepage

Static vs Dynamic Environments

Kamlesh Chandnani
May 4th, 2020 · 5 min read

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:

  1. local/development
  2. staging/pre-production
  3. 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:

  1. Static Environment Variables
  2. 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=8888
2CDN_PATH_PREFIX=https://mycdn.com
3SENTRY_KEY=123basdb12312
4ASSETS_PATH=/static
5STAGE=staging
6# 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';
2
3export 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

  1. Static web application
  2. Server rendered web application
  3. 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.js
2app.use('/environment.js', (req, res) => {
3 const API_HOST = process.env.API_HOST
4 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.js
2app.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.js
2app.use('/get-data', async (req, res) => {
3 const API_HOST = process.env.API_HOST
4 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.js
2const 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 variables
5 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 getEnvironment
13
14// server.js
15import getEnvironment from './environment.js'
16const { apiHost } = getEnvironment();
17
18app.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’s replace plugin if your environment variables are static.
  • Don’t use webpack’s DefinePlugin and rollup’s replace 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 the process.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 😀

Join the Newsletter

Subscribe to get the latest write-ups about my learnings from JavaScript, React, Design Systems and Life

No spam, pinky promise!

More articles from Kamlesh Chandnani

Jumping down the rabbit hole of GitHub Actions

Continuos Integration and Continous Deployment are an intrinsic piece of every software's workflow

April 27th, 2020 · 7 min read

Monorepos, Yarn Workspaces and Lerna! What the heck?

Monorepos are normal git repositories but it gives you the ability to have multiple projects inside one repository

April 20th, 2020 · 5 min read
Link to https://github.com/kamleshchandnaniLink to https://twitter.com/@_kamlesh_Link to https://www.youtube.com/playlist?list=PLpATFO7gaFGgwZRziAoScNoAUyyR_irFMLink to https://linkedin.com/in/kamleshchandnani