DEV Community

Cover image for You are reading environment variables the wrong way in Next.js
Austin Shelby
Austin Shelby

Posted on • Originally published at austinshelby.com

You are reading environment variables the wrong way in Next.js

If you have ever written code that looks like this:

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
};
Enter fullscreen mode Exit fullscreen mode

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}`
Enter fullscreen mode Exit fullscreen mode

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}`
Enter fullscreen mode Exit fullscreen mode

TypeScript will give us the following error:

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

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}`
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (20)

Collapse
 
larsejaas profile image
Lars Ejaas

I am working on a project in nextJS written in Typescript. I have gone one step further and implemented a library called Joi to validate .ENV variables. Problem with TypeScript is that it offers no validation on strings really. With Joi you can validate if the string is of the correct length and do validation using regEx.
This way your build will fail if someone did a typo in a crucial .ENV variable. Obviously it won't be bulletproof, but it will provide more confidence than simply checking that the variable isn't undefined.

Collapse
 
reikrom profile image
Rei Krom

Don't have to install >500kb package with 5x dependencies.

Another if statement in the config file with regex would let you do that.

Collapse
 
larsejaas profile image
Lars Ejaas

I totally agree with you. But somehow got the impression that Joi is super small.
Just looked it up on bundlePhobia.com, but Joi is 145 kB LOL πŸ˜‚πŸ™€ Not sure what I looked up the first time around!??
Back to the drawingboard making my own checks! But the idea is still valid- I think you should validate your ENV. variables...

Thread Thread
 
reikrom profile image
Rei Krom

It seems like a decent library to validate forms and I'd definitely consider it if i had to do enough of all kinds of different validations across the whole app. But then if i do form validation something like that could already be baked into a form package.

In this case seems like a bit of an overkill.

Thread Thread
 
larsejaas profile image
Lars Ejaas

I think I will make my own validation function to handle my use case. I basically want to validate things like string length and various patterns to avoid obvious typos in the. ENV variables.

Thread Thread
 
reikrom profile image
Rei Krom

If you are working with a build tool that supports tree shaking, you might be fine using the package as long as you only import the function you need and not the whole thing. Depends what you are working with

Collapse
 
nooxx profile image
Arno Simon • Edited

Does this still work?
Not working on my side because of this I think from the nextjs doc:

"Note: In order to keep server-only secrets safe, Next.js replaces process.env.* with the correct values at build time. This means that process.env is not a standard JavaScript object, so you’re not able to use object destructuring. Environment variables must be referenced as e.g. process.env.PUBLISHABLE_KEY, not const { PUBLISHABLE_KEY } = process.env."

Doing process.env.NEXT_PUBLIC_API_URL works, but not process.env['NEXT_PUBLIC_API_URL']

Collapse
 
darksmile92 profile image
Robin Kretzschmar

This is awesome! In a recent project we had various discussions of how to check if mandatory env vars are set and how to handle errors in Jenkins pipelines if the vars are not set on deployment target servers.

Being reminded of such simple and efficient solutions is great :)

Collapse
 
jochemstoel profile image
Jochem Stoel

I don't really see the point of all this. You received a very common error with a simple to find cause and fixed it. Then you wrote a whole post about environment variables in which you overcomplicate the thing entirely.

Most developers that work with Next are expected to know some way to properly deal with environment variables with things like dotenv but let's assume they don't.

Why should a HTTP request to get a blog post know anything about the process environment?

Although poor choice of words, I know what you mean there and you are right. It makes sense to say that it shouldn't rely on process.env existing in that context. This is why we separate our application logic, especially with JavaScript. You can of course, write a fetchBlog function that takes one or more arguments.

Somewhere in your utils.js or something:

// with one argument like in your example
export const fetchBlog = apiKey => fetch(`https://www.example.com/api/blog?api_key=${apiKey}`)
Enter fullscreen mode Exit fullscreen mode
// somewhere else where process.env is available and the logic is
import { fetchBlog } from './utils.js'
const { API_KEY } = process.env
fetchBlog(API_KEY).then(...)
Enter fullscreen mode Exit fullscreen mode

Often there is not just an API key. With multiple arguments you can do something like this and pass an object.

// with multiple arguments
const fetchBlog = options => fetch(`https://www.example.com/api/blog?api_key=${options.apiKey}&post=${options.postId}`)
Enter fullscreen mode Exit fullscreen mode

You can still insist on throwing exceptions simply inside the fetchBlog function.

// this function still has no idea what goes on in process.env
const fetchBlog = async options => {
    const { apiKey, postId } = options
    if(!apiKey) throw new Error(`Missing API key`)
    if(!postId) throw new Error(`Missing post id`)
    return fetch(`https://www.example.com/api/blog?api_key=${apiKey}&post=${postId}`)
}
Enter fullscreen mode Exit fullscreen mode

Tadaa, no matter if apiKey or postId are undefined or the API endpoint itself returns an error because the key/post does not exist or some other reason, you receive proper feedback error message.

But even then, your post started with a typo case. It is useful to ask yourself if a real world situation exists where that could happen, especially if it is your own application. If your code design patterns and abstractions are correct then you will have dealt with the missing environment variable way before any HTTP request is made.

I think it is a bit early to tell people what will make them a better software engineer.

Collapse
 
yatki profile image
Mehmet YatkΔ± • Edited

Hey @jochemstoel, I understand your point but I think verifying the necessary API_KEYs at the start of the application would prevent lot's of potential errors and we wouldn't make redundant api calls just to get an error.

This error also could happen during a crutial operation like a purchase or something... I think I'd prefer to check my API KEYS or TOKENS at the start of the application, at least this is what I took from this article.

Also, I understand the article title is a bit provocative but I wouldn't take these kind of things personally. I think author was just trying to get your attention, not to question your skills :D

Have a nice day πŸ––

Collapse
 
fenntasy profile image
Vincent Billey

There exists some drawback to this approach though: if you want to package your app in a docker container, that would mean you need your API_KEY at build time in the docker environment and this is a known vulnerability because you would store your API key in the docker layers and it could be accessed within the docker image.

I'm not saying your method is not a good idea (it is) but it shouldn't be so absolute either as it can backfire at you.

Collapse
 
yatki profile image
Mehmet YatkΔ± • Edited

Not really right, because your application won't be run inside the docker until you run the image. You'd provide the environment variables with docker run command or with a docker-compose file. So there wouldn't be any problems during the build πŸ––

Collapse
 
fenntasy profile image
Vincent Billey

Yes, sorry, it will only happen if you run the build command when building your docker container.

Collapse
 
yatki profile image
Mehmet YatkΔ± • Edited

Actually I really like this approach. It sounds like a best practice to me.

I have library to read environment variables as application config. I was thinking to add it to the library, having an option like mandatoryKeys but then I left it to users to validate the keys. Maybe adding that option and making it mandatory to define could force this practice. If they don't want to validate the keys they'll have to define mandatoryKeys: [] for instance. I'll think about it.

GitHub logo yatki / read-env

πŸ”§ Transform environment variables into JSON object with sanitized values.

read-env

Transform environment variables into JSON object with sanitized values.

NPM version npm Coverage Status Dependencies

See docs for previous version v1.3.x.

Main purpose of this library is to allow developers to configure their applications with environment variables. See: a use case example.

What's New with v2.x πŸš€

  • Migrated to Typescript, Yay! πŸŽ‰
  • Simplified API
  • With new separator option,nested object constructions are possible.
  • New source option allows you to use other objects, other than process.env

Migrating from v1.x to v2.x

  • default export is deprecated. Please use named export readEnv as below:
const { readEnv } = require('read-env');
// Or
import { readEnv } from 'read-env';
// Or in browser
window.readEnv('EXAMPLE');
Enter fullscreen mode Exit fullscreen mode
  • parse option was renamed as sanitize.
  • transformKey option was renamed as format.
  • Deprecated options: ignoreInvalidJSON, prefix, filter,

Install

npm install --save read-env

or

yarn add read-env

Basic

…

Thanks for the tip.

Cheers, πŸš€πŸ––

Collapse
 
a7med3bdulbaset profile image
A7med3bdulBaset

I used to add auto-completing to my env vars using TypeScript like this:

namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: "development" | "production" | "test";
    APP_URL: string | undefined;
  }
}
Enter fullscreen mode Exit fullscreen mode

While I'm coping your function add a small type:

export function getEnvVar(varName: keyof NodeJS.ProcessEnv): string {
  const unvalidatedEnvironmentVariable = process.env[varName];
  if (!unvalidatedEnvironmentVariable) {
    throw new Error(
      `Couldn't find env variable: ${varName}`
    );
  } else {
    return unvalidatedEnvironmentVariable;
  }
}
Enter fullscreen mode Exit fullscreen mode

But, unfortunately, seems like the keyof NodeJS.ProcessEnv does NOT work.
Do you have any idea how to solve this?

Collapse
 
sokol8 profile image
Kostiantyn Sokolinskyi

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.

I believe this statement if factually incorrect. The application will compile and will run into a runtime exception upon startup and crash the server.

Provided approach will not validate if all env variables exist before trying to run the server. Correct me if am wrong.

Can you suggest an approach to validate existence of all Env Vars during
nest build step?

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh • Edited

Env Var is a perfect package for this npmjs.com/package/env-var

You can make a variable as required, parse from base64, other types. It’s been my goto recently

Collapse
 
austinshelby profile image
Austin Shelby

Great suggestion. That package looks great for this purpose.

Collapse
 
jbaczuk profile image
Jordan Baczuk

Thanks for the idea. To truly get it to throw during build, I added a custom plugin to the webpack config, similar to: stackoverflow.com/a/72277530

Some comments may only be visible to logged-in visitors. Sign in to view all comments.