NextJS per Environment App Config

Who Should Read This?

This article is intended for developers who want to take advantage of the benefits and optimizations of the NextJS build process AND be able to change application configuration at runtime based on the environment the application is deployed within (i.e., “build once, run anywhere”, or BORA). The purpose of this article is to look at the problem and talk about solutions, so it won’t get into the debate between using per environment application configuration versus the 12-factor app methodology.

A Little History

On numerous occasions, the NextJS team has stated that they believe application configuration should be set at build time because this follows the guidelines of the 12-factor app. However, in the past, they have provided escape hatches for developers who want BORA applications. One of these escape hatches was publicRuntimeConfig.

Prior to version 9.3.x, developers could use dynamic environment variables to change the value of publicRuntimeConfig in next.config.js at runtime. An overly simplified version of this might look something like below:

// next.config.js

module.exports = {
publicRuntimeConfig: process.env.MY_ENV ? { env: process.env.MY_ENV } : { env: 'myDefault' }
};

With the set up above, myEnvVar could be undefined at build time, which would use the default origin, and then be defined at runtime in each environment to supply a custom value per environment. If you haven’t already put two and two together, this is one way to change application configuration in each environment while using a single build (i.e., BORA).

If you need a refresher on what publicRuntimeConfig is in NextJS, please take a look at the link below.

https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration

If you’re wondering what it is and how next.config.js works, the link is below.

https://nextjs.org/docs/api-reference/next.config.js/introduction

The Problem

Upgrading from NextJS 9.2.x to 9.3.x or higher brings a change to NextJS that is not clearly outlined in the NextJS documentation. This change is not specific to how publicRuntimeConfig is implemented within the NextJS framework, but it does have a significant impact on it. The change in 9.3.x and beyond essentially hardcodes publicRuntimeConfig at build time (if you want more than the tl;dr version, scroll to the bottom of this article to the section titled “A Detailed Explanation of How Versions 9.3.x+ Break publicRuntimeConfig”). This means that in the sample code above, whatever the value of publicRuntimeConfig is evaluated to during the build will be used in the application regardless of whether myEnvVar is changed in a deployed environment. If you had an application that was using publicRuntimeConfig in a similar way to the code above, you most likely found that upgrading beyond 9.2.x caused your application configuration to stop working the way you expected. If you are asking yourself what evidence I have to support this claim, scroll down to the section titled “Where’s the Beef?”

One of the Solutions

If you have read through the NextJS documentation, you may be thinking that the solution is to use .env files for application configuration. Unfortunately, that won’t help you if you are trying to create a BORA application. The reason is because the .env files are read only once, at NextJS build time.

This screenshot was taken from the NextJS documentation and states that it replaces the value of process.env at build time.

So, how do we avoid throwing the baby out with the bath water (i.e., continue using NextJS AND maintain a BORA application)? There are several ways to get around this issue that are outlined in blog posts written by others, but this article will focus on a solution that I have not seen mentioned anywhere else in detail, application configuration using NextJS API routes.

At its root, this uses the method of loading application configuration through an API request; however, it also takes advantage of a NextJS feature that I have not seen talked about all that much, the ability to create API routes using NextJS. The benefits of this approach are twofold. 1.) You do not need to create a separate backend server to use this method, and 2.) both your client side and server side code can be kept in the same repository.

If you are not familiar with this feature of NextJS, check out the link below.
https://nextjs.org/docs/api-routes/introduction

Here is some sample code to show how this could work.


// /pages/api/config/index.js

export default function handler(req, res) {
const config = process.env.MY_ENV ? { env: process.env.MY_ENV } : { env: 'myDefault' };
res.status(200).json(config);
};

When you look at this, you may wonder how it works since NextJS replaces process.env at build time. The answer is that API routes modules are not compiled with hard coded values at build time the same way that _document.js is (read more about this in the section “A Detailed Explanation of How Versions 9.3.x+ Break publicRuntimeConfig”), so this module will pick up the value of myEnvVar when the application is running.

Then, in /pages/_app.js, make an API request to /api/config.


// /pages/_app.js

import App from 'next/app';
import axios from 'axios';

class MyApp extends App {
async componentDidMount() {
const config = await axios.get('/api/config');

// NextJS Babel version handles optional chaining
appConfig = config?.data;

if (window) {
window.__MY_APP_CONFIG__ = appConfig;
}
}

render() {

// Render app
...
}
}

export default MyApp;

A Detailed Explanation of How Versions 9.3.x+ Break publicRuntimeConfig

During the NextJS build phase, the NextJS build engine compiles all of the application code and attempts to maximize any optimizations by creating a static index.html file that is created at /.next/server/pages/index.html that references all the JavaScript bundles. During this step, the NextJS build engine runs and evaluates next.config.js in order to add any customizations for the application to the built files. Running and evaluating next.config.js includes evaluation of any logic for determining the value of publicRuntimeConfig. During the build phase, NextJS adds a script tag with an ID of __NEXT_DATA__ to the static index.html file. This script tag includes an attribute with a key of runtimeConfig which has a value of whatever is evaluated from next.config.js for the publicRuntimeConfig attribute. Since the value of publicRuntimeConfig is added as a static value in the index.html file, changing what the value for publicRuntimeConfig evaluates to in next.config.js after the build completes will have no effect on the data available to the application.

Just in case you had trouble following all the things in that last paragraph, here is the same information in the form of an example.


// next.config.js

module.exports = {
publicRuntimeConfig: process.env.MY_ENV ? { env: process.env.MY_ENV } : { env: 'myDefault' }
};

If the next.config.js is set up as above AND the environment variable myEnvVar is not set, then after running the next build command from the command line, a file will be created at /.next/server/pages/index.html, and that file will contain a script similar to what is below.


// .next/server/pages/index.<wbr />html

<script id="__NEXT_DATA__" type="application/json">
{
"props": {
"pageProps": {}
},
"page": "/",
"query": {},
"buildId": "abc-123",
"runtimeConfig": {
"env": "myDefault"
},
"nextExport": true,
"autoExport": true,
"isFallback": false
}
</script>

Since the value is hardcoded into this file, it will not change when the application is run in a deployed environment regardless of what the value of publicRuntimeConfig in next.config.js evaluates to in that environment.

If you’d like to play around with this yourself, head over to the Github repository below and follow the instructions in the README file.

https://github.com/NFabrizio/nextjs-publicRuntimeConfig-test

Where’s the Beef?

So, you don’t believe me and/or you want to see some evidence that proves what I said about the hard coding of the publicRuntimeConfig at build time. Here are the steps I followed to test this:

  1. Ran the application, and noted that the application displayed “myDefault” as the value of publicRuntimeConfig.env.
  2. Stopped the application.
  3. Set the environment variable.
    – MacOS: export MY_ENV=local
    – Windows: set MY_ENV=local
  4. Ran the application again, and noted that the application still displayed “myDefault” as the value of publicRuntimeConfig.env.
  5. Stopped the application.
  6. Removed the build files.
  7. Set the environment variable again for sanity.
    – MacOS: export MY_ENV=local
    – Windows: set MY_ENV=local
  8. Rebuilt the application.
  9. Ran the application again, and noted that the application then displayed “local” as the value of publicRuntimeConfig.env.
  10. Stopped the application.
  11. Unset the environment variable.
  12. Ran the application again, and noted that the application still displayed “local” as the value of publicRuntimeConfig.env.

 

If you’d like to try this for yourself, head over to the Github repository below and follow the instructions in the README file.

https://github.com/NFabrizio/nextjs-publicRuntimeConfig-test