At Hardfin, the Next.js framework provides a powerful way for us to build frontend applications. It comes loaded with features that we use on a daily basis — however one area that is limited in functionality is support for multiple build environments.

Backing up a bit, Next.js build environments are fundamentally powered by environment variables, and these environment variables allow us to configure different settings for our Next.js application without hardcoding them. This is especially useful for configuring things like API endpoints based on the build type — for example, you might have multiple APIs that serve development, staging, and production data. Next.js supports environment variables by relying on .env files to differentiate development and production builds.

Only two environments!?

Next.js does in fact only support two different build types1 — development and production. This is fairly restrictive and there have been numerous issues opened and RFCs written to expand the built in environment-loading capabilities for Next.js. As of now though, projects that require operating across more than two environments need to get creative with their solutions.

Get creative

At Hardfin, we build our Next.js applications across several deployments such as development, staging, demo, and production. In the next section, we’ll walk through how we used a simple, custom script that allows us to define any number of different environments.

Next.js environment loading script

The first step to configuring environment variables is to understand how environment variables can be loaded. It turns out that Next.js has a built-in way to set environment variables at build time through the next.config.js file. Specifically, if a property called env is exported, all of the keys specified will be available in the build as environment variables. This is similar (but not identical) to2 how environment variables are loaded from a .env.development or .env.production file. For example, if we wanted to configure an API_ENDPOINT environment variable, it might look something like the following:

// next.config.js
module.exports = {
  env: {
    API_ENDPOINT: 'http://localhost:1234'
  }
};
// src/get-weather.ts
export function getWeather() {
  return fetch(`${process.env.API_ENDPOINT}/weather`);
}

Now, if we wanted a different value for API_ENDPOINT based on something like the target environment, we could modify our next.config.js script to include this behavior:

// next.config.js
function getAPIEndpoint() {
  if (process.env.TARGET_ENVIRONMENT === 'production') {
    return 'http://api.example.com';
  }

  if (process.env.TARGET_ENVIRONMENT === 'staging') {
    return 'http://api.staging.example.com';
  }

  return 'http://localhost:1234'
}

module.exports = {
  env: {
    API_ENDPOINT: getAPIEndpoint()
  }
};

The above example is a good illustration of how we might set different environment variable values based off the environment, but the syntax is a bit verbose and cumbersome. We can improve on this by mimicking the existing environment variable patterns. We would do this by:

  1. Specifying an env/.env.{environment} file for each environment (env/.env.development, env/.env.staging, etc…)3
  2. Reading the appropriate file based off of the environment using the Node.js fs package.
  3. Parsing the file using the dotenv package
  4. Exporting the results through the env key in next.config.js

The result will look something like this:

.
├── env
│   ├── .env.demo
│   ├── .env.development
│   ├── .env.production
│   └── .env.staging
├── src
│   └── ...
├── next.config.js
└── package.json
// next.config.js
const dotenv = require('dotenv');
const fs = require('fs');
const path = require('path');

function loadTargetEnv(target) {
  // Constructs a path such as env/.env.development
  const envPath = path.join(__dirname, 'env', `.env.${target}`);
  return dotenv.parse(fs.readFileSync(envPath));
}

module.exports = {
  env: loadTargetEnv(process.env.TARGET_ENVIRONMENT)
};

Conclusion

While Next.js doesn’t provide default support for multiple environments, it’s possible to emulate the exact desired behavior by leveraging its configurable build process. Here, we’ve outlined a very basic script to load environment variables but this process can be as customizable as your application needs!


  1. Next.js also supports a third build type specifically for automated test environments. See the documentation for .env.test↩︎

  2. See the documentation for the env property to see how it differs from loading variables via .env files ↩︎

  3. The .env files are being placed in an env/ directory so that the default Next.js behavior for .env files is avoided. ↩︎