How to inject environment values in JavaScript app with Webpack

Let's say you have a JavaScript web app, built by Webpack, in which you have some hard coded API keys (for example Firebase, Google Maps etc). Now, you want to avoid hardcoding the API keys (as a good developer that you are) and at the same time provide different set of API keys for each enviornment - dev, QA, production, etc.

If you have a backend, serving your app, then it's pretty trivial to accomplish. But, what if you do not have a backend, and you serve your JavaScript app as a static asset by Nginx? How will you achieve the separation of API keys?

Then you do it during the build time. You need to build your app, passing API keys (or whatever values that you need to be injected) as parameters to the build pipeline and have them replaced at the right places.

Injecting values during build

With Webpack, you can do this with the html-webpack-plugin. You can define arbitrary key value pairs with this plugin configuration and read this in your templates. The plugin will take care of replacing the correct value during build time.

For example, Lets look at the below webpack.config.js:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // other webpack configuration
  ...
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/view/index.html',
      apiKeys: {
        google: 'GOOGLE_API_KEY',
        firebase: 'FIREBAE_API_KEY'
      }
    })
  ]
};

In the HtmlWebpackPlugin configuration, along with plugin options, we have also defined our own key value pairs apiKeys. Then in your template file, index.html, you can access this value as htmlWebpackPlugin.options.YOUR_KEY_NAME. For example,

<!doctype html>
<html>
  <head>
    ...
    <script type="text/javascript">
      <script type="text/javascript">
      window.RESOURCES = {
        GOOGLE_API_KEY: "<%= htmlWebpackPlugin.options.apiKeys.google %>",
        FIREBAE_API_KEY: "<%= htmlWebpackPlugin.options.apiKeys.firebase %>",
      };
    </script>
    </script>
    ...
  </head>
  ...
</html>

In the above example, I read the build time values and set them into window scope so that I can access it anywhere in my script.

Providing environment specific values to build pipeline

With above setup, we moved the hardcoded API keys from code layer to build layer. But the values are still hardcoded. How can we avoid that?

By setting the values as Environment variables and letting node read and supply it to us with process.env. Let's change our HtmlWebpackPlugin configuration a little bit:

plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/view/index.html',
      apiKeys: {
        google: process.env.GOOGLE_API_KEY,
        firebase: process.env.FIREBASE_API_KEY
      }
    })
  ]

With this, all you have to do is to set the correct environment variables in the machine, thus completely avoiding the hardcoded values in our code base.

If you want to supply different API keys for different environments, you can either pass in the values in command line when you invoke the build script or if you have a CI pipeline, define the environment values in the prod / dev build jobs.

As an alternative to setting up environment variables, you can also use the module dotenv to define the variables as a dot file in your project's root folder. But remeber NOT to commit the dot file in Git, as it allows each system to supply it's own environment variables.

So, that's it. What do you think about this approach? Please let me know in the comment section.