How to build front-end apps once & reuse across environments

Anil Chaudhary
Sequoia.com
Published in
6 min readMay 4, 2020

--

A beautiful afternoon, working on regular stuff. Everything was going smoothly. Then a notification pops up on my screen saying `Hi`

who’s that?

Found out it was our DevOps Architect. I replied `Hi`. I thought he pinged me for some regular build related query.

We will be calling Our DevOps Architect as D.A in the rest of the article.

But next ping was a start to a very interesting problem and at the same time challenging.

He asks me this question

Can we separate the env generation part and the build process? , so that we can use the same docker image for various environments.

Before I move on to my reply. Let me give you some more context. So generally for most of the frontend applications. We keep endpoints, analytics keys, and other keys at a cloud-based key-value pair store. These parameter store values are fetched before building for any environment(dev, staging, UAT, pre-production e.t.c). We fetch these param values and set them to process.env

process.env.REACT_APP_PARAM_ONE={SOME_VALUE_FROM_PARAMSTORE}

By doing this, we make these values available in our build process. This leads to the creation of a build folder containing correct param values. That’s the process we almost always follow.

Moving on to the conversation

Me: The env generation part cannot be done after the build is done. Because by the time build is done, all the param values are already in your bundle code. There is no way of injecting environment-specific values later on.

But then, He has already done a good amount of research around this topic. I was not ready enough. So we started having more conversations.

D.A: What about backend ?. They are able to successfully do it. We are able to use the same build for various environments.

I gave a thought for some minutes and then

Me: Well, for them it is quite easy to do so, as their’s code runs on the server-side, where they can keep picking param values from memory(RAM). But for frontend apps, where we statically build, bundle code, and ship it to user browser, it is no way possible. For frontend apps Run Time itself is not there on the server, it’s in the user’s browser.

D.A: Ok. But right now we are repeating the build process in all the environments for the same code. This is not optimal. We need to find a way for that separation.

Me: Yes, definitely. I understood the problem. Give me some time to think about it.

Our D.A in the meantime searching all on the web for all the relevant articles on this topic.

He came up with a few articles and threw them at me in the chat.

Actually it’s always a difficult scenario when a team member tells you that it is easy peasy. It might look easier from the outer perspective. Instead of going into defence mode, you have to come up with a justifiable reason, why something can’t be done in your scenario.

Healthy crucial conversations always leads to innovation.

I have learned this hard way and thought to convert this problem into an opportunity to innovate. When we work together as a team and practice positive criticism, we grow. I should thank our DevOps Architect in this case for pushing me into figuring out a solution to our problem.

So now, I thought well, let’s read these articles. They actually helped me to visualize the solution at hand with more clarity, but the solutions mentioned were either non-generic or non-secure.

For example. Putting a separate .js file per environment in your hosted folder is not at all a safe idea. Because once they are loaded on the client-side, they need to be added in the global scope to allow their access everywhere. But the global scope is available for each and every script. Any third party script can update the param values and steal the data. if needed.

By this time, I was very clear about my goals for this problem. Let’s highlight them below.

  • The solution should not change the way developer write code.
  • The Developer experience should not hamper at any cost.
  • Most of the build process work needs to be reused except param value injection which is environment-specific.
  • It should be blazing fast. (consecutive builds).
  • The solution should be very generic at the conceptual level. More on this later.

So now we know the high-level goals.

How it works

This all happened over the span of 2–3 days. By then, I have got some ideas and was able to draw the problem.

Problem Statement

Now the REACT_APP_TEST will be replaced with `https://some/envspecific/api` after the production build. But the Param value is specific to the staging environment. We can’t use the bundle generated after step1 for another environment. We have to repeat the complete build process for all the environments. This is the problem statement.

Now, what if we can make use of the bundled code without building again for various environments. How cool that would be !!. We will save a lot of time, network bandwidth and processing on our servers. I started thinking 🤔

If we are able to prevent the param values to replace in the build process, we can get a second step where we can just inject these param values. Injecting param values will take a minimal amount of time, but all the heavy lifting done by the build process will not be repeated.

Solution

So as we can see above, The desired process was to allow a second step where we do the injection. All good on theory. But How do we do it?

Our project uses webpack and it is responsible to bundle the code. We have to tell webpack to not transform and evaluate certain values.

The examples of types of values are :

process.env.REACT_APP_NAME
process.env[“REACT_APP_PARAM_ONE”]
<div className="App">
{process.env["REACT_APP_PARAM_THREE"] === "I AM PARAM THREE" && (
<p>Conditional got rendered</p>
)}
</div>;

webpack uses babel for loading .js files in our project. It is responsible to transpile our code to a module ES5 output.

So this article is more on usage of the solution rather than the complete implementation. We will be covering usage of packages created in the whole process thoroughly and leave an opportunity for another article if needed.

So I ended up creating a webpack loader which makes use of babel parser to replace the param value with some placeholder values giving us a second chance for replacement.

The second step surely needs some kind of script which does the correct replacement of values. To address all this, I ended up creating a GitHub repo and npm package. Here is the link to those:

The link to npm package is :

The readme file is updated with the latest docs, can be referred for the usage.

Finally, I was able to make use of the same build across multiple environments. I will be updating this post on the final improvements in deployment times within a few days.

One learning everyone can take with this story is, how a problem can be a beautiful chance for learning and doing new things. These problems are the reason for innovations. Challenge your problems and that too with positiveness. Things will fall your way.

We at Sequoia Consulting Group, believe in a culture where we push each other for innovation to build world-class products.

Reach out to me to know more about the team and the culture. Surely check out our career page

Thanks !!

--

--

Anil Chaudhary
Sequoia.com

@Google Javascript | Developer Experience | Dev Rant