Updated 2017-02-12: Added a strategy for loading configuration at initial page load, using an “env.js” file.
Recently, I needed to inject some configuration variables into my client-side app. Because Googling for it turned up a lot of “well, you could maybe do this…” I decided to write down what I see as the 3 main options:
- Best in show: Load configuration at run time.
- Prize for simplest solution: Build your application with configuration variables as constants at compile time.
- Runner up: Use server-side templating to inject configuration at render time.
So, what do I want to do again?
First, let’s define the problem clearly. Handling configuration variables is something most applications need. For server-side applications, there are plenty of options out there. Most involve loading properties from files and then overriding values with environment variables injected at run time - such as the config package for node servers.
Finally, I want to follow the recommendations for configuration of the Twelve-Factor app, which boil down to:
- “strict separation of config from code”
- “…stores config in environment variables” rather than environment specific config files
So, let’s examine the options, and consider the pros and cons of each.
Load configuration at run time
This is strategy I went with, and seems to be working well. With this approach, the client app loads configuration from a remote server on initial page load.
We also do not have to use any server side templating, which gives us more freedom in how we deploy and serve our application. We can serve our application as static files if we want to.
Here is the gist:
- In your
index.htmlfile, add a script tag for an “env.js” file, which does not exist. (yet)
- In your application server code, write the “env.js” file on startup using an environment variable as the contents. Then statically serve the directory containing the “env.js” file.
- Make the environment variable available to the server when running it.
One of the downsides here is that we are creating global state, the
config variable, which is something that is
to address this, with the simplest being to agree not to read from the
config variable directly, and create a function that your application always uses to access it. This way, the only place in your codebase where the config variable is touched is that function. You should also never modify the config variable. Treat it as read only.
An experiment - asynchronous configuration loading
Along similar lines, I also played with loading configuration from a remote server with an XMLHttpRequest. This feels like a cool approach, but the wrench here is that introducing asynchronous network calls introduces un-necessary complexity. Now, all of a sudden your config properties are not available until that call comes back. This can cause some awkward uses based on how/when you need your environment config.
To play with this idea, I wrote this unreleased npm package. Check it out if you are interested and provide feedback or make a pull request! xhr-env-provider
Build your application with configuration variables as constants at compile time.
This is probably the most straight-forward option, and uses your build tool to inject constants into your code as global variables at compile time. The DefinePlugin is the best way to do this if you are bundling your app with webpack.
The downside with this approach is that if your configuration varies for each deployed environment (which it surely will), you will have to build multiple versions of your app. This is less than ideal, as it adds extra steps to your build and deploy process. It also means that the asset you use in lower environments (dev) is not the same as the asset you use in production, because you have re-compiled it.
However, it is likely that you will compile different versions of your app anyway. For example, you probably won’t enable source maps for your production build. Webpack support this concept. http://webpack.github.io/docs/cli.html#development-shortcut-d
This technique is best suited for externalizing configuration that does not change throughout your build and deploy process. Turning console logging on/off, for example. Locally, you may want console logging to be on, but in all deployed environments you may not want to be doing that.
Use server-side templating to inject configuration at render time.
In other words, just throw some globals into the
window object when the page is initially rendered. Here is an example using Pug (formerly known as Jade)
Two things here that are not so great. First, we are creating global state again. Second, we have to introduce a dependency on a templating engine. This means we can no longer just serve our client application as static assets. With option #1, we could have used something like Amazon S3 to deliver our client app, while now we have to maintain our own server in order to do the templating.
This approach has more of an old-school feel to it with the server-side templating and the global window variable.
It might be fine for you though if you are already using server-side templating. This is pretty much the way I did it in the last Rails app I worked on (back in 2014).
I think being able to injecting env properties at runtime is the way to go for large-scale production applications, as it gives you the most flexible options for local development and deployments. But, if you want a quick win for a side-project or small app, injecting at compile time is super easy. I probably wouldn’t use the server-side templating unless I was working on an application that was already doing that.