Immutability principle vs Angular build
In software design, we have many principles that allow us to avoid problems in the future. One of them is immutability. It stands for: once we create an artifact for one environment, we shouldn’t rebuild it for other environments. We should re-use the artifact for other environments instead of creating a new one each time we’d like to deploy it to higher environments.
Why? Because it allows us to be pretty sure that nothing is changed instead of configuration. During the build process, there are too many points where we don’t have control. There is a risk that two builds, executed in different time, can behave differently. It could be a different library version, a different compiler version or different default settings, etc.
In case we rebuild artifacts each time, we need to test it again to be sure everything is working fine. It’s not optimal. It’s better to design it as generic and configurable, build it once, test and re-use for other environments. This is the immutability principle.
Let’s imagine we have a backend application built using Docker. How can we parameterize it to be generic? I.e. by passing environment variables. Each environment (dev, qa, staging, prod) can have different value. It’s easy to implement during deployment. Just set the environment variable during deployment.
In frontend application we’d like to set API URL for example. We cannot do this way. The application is running on client’s browser. We cannot set environment variables during deployment. Angular build (simplifying) converts TypeScript code into JavaScript code that can be run in the browser, and we can serve these files only.
We have several options how to set API URL:
- Hard-code it.
- Base on host name from URL.
- Set it dynamically somehow.
Many projects hard-code it. It’s also supported (and recommend?) by Angular. By setting a proper --configuration
during build process, we can select which API URL can be used. As we already know, this approach doesn’t follow the immutability principle, because we have to rebuild the artifact for each environment.
The second option is to base on hostname from URL. I.e. our frontend application has URL: https://myapp.example.com and API URL is https://api.myapp.example.com. We can just add api.
prefix to grabbed hostname of frontend application. I don’t recommend this way because it’s very limited. We need to have static prefix for API URL, and we cannot expose frontend application on different URL and reuse API URL.
The third option is to set API URL dynamically somehow, but how? There’s no way to set it by environment variable. Or isn’t? Let’s imagine we’re creating a Docker image that contains our frontend application served by nginx. We’re creating Dockerfile that copies our js files. In CMD section we can execute something just before we run HTTP daemon. We can replace some strings in our configuration, i.e. by envsubst
or sed
. We can replace API URL by value passed by environment variable. This variable is set in the running container, we can access it and replace the Angular configuration file. Example:
CMD ["/bin/sh", "-c", "envsubst < /usr/share/nginx/html/assets/env/env.template.js > /usr/share/nginx/html/assets/env/env.js && exec nginx -g 'daemon off;'"]
The last method is simple, flexible and follows the immutability principle. I use and recommend it.
How do you cope with it?