Runtime configuration of a static JS build

2020-01-20

Written by Guillaume Moigneu

It’s considered a best practice to build your application in an immutable way. No matter the environment you are running your app in, you are running the same artifact. It ensures consistency over your different environments and test stages.

You then inject an environment-specific configuration to make sure the app has the correct settings based on the environment like an API endpoint that will be different between production and development.

Unfortunately, this is not possible with all webpack based tools and frameworks when you don’t have any server side rendering (SSR). webpacks imports all modules during the build phase and has no way to override this due to its nature.

When you are running a JS project with a node backend (Universal, SSR, etc), you can always inject some configuration (like environment variables) at runtime using process.env but this is not possible when only serving a fully static build.

So how do we inject those specific environment configurations?

  1. Make everything based on formulas. It can be useful for API urls for example. If your frontend is connecting to https://api.<the-same-hostname>, then it is worth is to just replace that by window.location.hostname. But it falls short when the configuration value can’t be guessed.
  2. Hardcode the values with a switch. That’s just bad. It can work if you have fixed environment but not with dynamic ones like Platform.sh provide.
  3. Load and inject a configuration file at runtime. We are going to dig deeper into that solution.

Example with Nuxt.js

You can browse this sample code at https://github.com/gmoigneu/js-runtime-config

Load the configuration

So our app runs in the client browser. That means we can’t get any info from the server without actually making a new http request to a public configuration.

Let’s do this in one of our component:

await axios.get('/run/config.json').then(m => {
    ...
}

We can now use the JSON values inside that file to customize our runtime. Let’s do a XHR call to our API with the token we got in our .json:

await axios.get('/run/config.json').then(m => {
    axios.get(window.location.protocol + '//api.' + window.location.hostname + '/?token=' + m.data.token)
    .then(response => {
        this.message = response.data.message
    })
})

We are storing the response message in our component data to display it.

But how is this configuration generated?

The json can for sure be written by hand but let’s use the tooling that Platform.sh provides to generate this.

First, we need an environment variable. Let’s define one as JSON. I’ll name it NUXT:

variable

Please note that I name it env:NUXT in order to have a real environment variable available.

Then Platform.sh goes through 2 different steps when you deploy:

  • The build step where environment specific configurations are not available
  • The deploy step that knows the environment and is run just after the new container has been put into service

We are going to define a new deploy action that will trigger a script:

hooks:
  deploy: |
    set -e
    ./build-config.sh

Our build-config.sh will be like this:

#!/bin/bash
echo $NUXT > ./static/run/config.json

This is a really basic script that just outputs the variable content in a file. Feel free to play with more complex actions by using jq for example.

This configuration will generate our config.json file everytime we deploy an environment.

Two important things to note:

  1. The config.json file must be publicly available (the static folder is used on Nuxt for that purpose)
  2. That folder needs to be writeable by the app. Here is the Platform.sh configuration:
mounts:
    'static/run':
        source: local
        source_path: run

The config.json being publicly available, you can’t put any secret in it!

Result

result

As you can see, our frontend JS is now doing a first XHR call to get the config.json object and then properly calling the API with the right token.

Improvements

My example is more a proof-of-concept than anything else. Nuxt and Next provide ways of handling this through plugins and modules. You should definitely load the configuration once and then reuse the values, not loading it on each components.

Based on my research on webpack, it should be possible to load a dynamic module through require.ensure. Unfortunately, I couldn’t get it to work properly. If you have any insight on this, feel free to get in touch!