The Correct Way to Load Environment Variables in Next.js

I have searched this topic a lot on the internet and I have not once seen this done correctly.

This will be the only post you will ever need on this subject, and I can guarantee you will never again crash your production environment because you mistyped API_KEY.

If you have ever written code that looks like this:

const url = `https://www.example.com/api/blog?api_key=${process.env.API_KEY}`

Then you are doing it wrong!

Here's why this is a bad idea.

In a scenario where you build the application without having set the API_KEY environment variable the application will use undefined instead.

Obviously undefined is not the correct api key which will make any request using that URL fail.

The problem here is that when the error surfaces, the message will be very misleading and look something like this:

Error: Unauthorized

And this error will only show up when you try to use the url to fetch the blog posts.

If fetching the blog posts is an essential feature, the application should not have even compiled without the api key being available.

Naively expecting the API_KEY environment variable to exist will hide the bug and make this problem a pain to debug due to the misleading error message.

To fix this issue we need two things.

  1. When a problem exists that causes the application to not function, the application needs to fail immediately and visibly.
  2. A meaningful abstraction to encapsulate the loading of environment variables.

How to load environment variables in Next.js

This works with any node.js application. Next.js just makes this easier, as it comes with a lot of necessary boilerplate code.

Let me show you how to use environment variables in Next.js correctly, and then explain why this works.

Create a .env.local file. Here you will put all of your environment variables you want to use on your local development environment.

API_KEY=secret

Next.js automatically adds this file to .gitignore so you don't have to worry about it ending up in your version control system.

If you are using any other framework than Next.js you need to use a package like dotenv to read the environment variables from a file.

Now to the bread and butter.

Create a config.ts file with this code to read the environment variables into your config.

const getEnvironmentVariable = (environmentVariable: string): string => {
  const unvalidatedEnvironmentVariable = process.env[environmentVariable];
  if (!unvalidatedEnvironmentVariable) {
    throw new Error(
      `Couldn't find environment variable: ${environmentVariable}`
    );
  } else {
    return unvalidatedEnvironmentVariable;
  }
};

export const config = {
  apiKey: getEnvironmentVariable("API_KEY")
};

And change code that we wrote earlier into this:

import { config } from "./config"

const url = `https://www.example.com/api/blog?api_key=${config.apiKey}`

Why this is the correct way to load environment variables

In a case where you forgot to add the environment variable API_KEY the application won't even build/compile, and it will throw an error like this: Couldn't find environment variable: API_KEY.

Our application now fails immediately and visibly.

This is called failing fast.

It is part of the clean code principles, which you can read more about here: https://www.martinfowler.com/ieeeSoftware/failFast.pdf

Because we are using TypeScript, we can be 100% sure that all the values in the config exist.

Additionally, TypeScript helps us avoid small bugs.

If we make a typo:

const url = `https://www.example.com/api/blog?api_key=${config.apiKeu}`

TypeScript will give us the following error:

Property 'apiKeu' does not exist on type '{ apiKey: string; }'. Did you mean 'apiKey'?

How cool is that!

It's like coding with superpowers.

Encapsulating logic

Let's look at the example we started with:

const url = `https://www.example.com/api/blog?api_key=${process.env.API_KEY}`

Do you notice that process.env part there?

Why should the functionality of fetching blog posts know anything about the user environment the application is currently running in?

Well it shouldn't.

The logic of fetching blog posts doesn't care where it gets the api key from. If it comes from the user environment, text file, or an API doesn't make any difference to it.

Therefore, it shouldn't rely on process.env or any other low-level abstractions.

Creating a config for the sole purpose of reading environment variables encapsulates this functionality and creates a meaningful high-level abstraction.

A config.

Thanks to this, we can change the way we get the config values (like the api key) without touching the blog post functionality at all!

Another very hidden benefit is that unit testing just became ten times easier. Instead of playing around with our user environment, we can just mock the config with the values we want to.

Conclusion

While this might seem pedantic, keeping these small things in your mind while writing code will make you a better software engineer.