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.
- When a problem exists that causes the application to not function, the application needs to fail immediately and visibly.
- 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.
Top comments (20)
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.
Don't have to install >500kb package with 5x dependencies.
Another if statement in the config file with regex would let you do that.
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...
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.
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.
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
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']
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 :)
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.
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:
Often there is not just an API key. With multiple arguments you can do something like this and pass an object.
You can still insist on throwing exceptions simply inside the fetchBlog function.
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.
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 π
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.
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 πYes, sorry, it will only happen if you run the
build
command when building your docker container.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 definemandatoryKeys: []
for instance. I'll think about it.yatki / read-env
π§ Transform environment variables into JSON object with sanitized values.
read-env
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π
separator
option,nested object constructions are possible.source
option allows you to use other objects, other thanprocess.env
Migrating from v1.x to v2.x
default
export is deprecated. Please use named exportreadEnv
as below:parse
option was renamed assanitize
.transformKey
option was renamed asformat
.ignoreInvalidJSON
,prefix
,filter
,Install
or
Basic
β¦Thanks for the tip.
Cheers, ππ
I used to add auto-completing to my env vars using TypeScript like this:
While I'm coping your function add a small type:
But, unfortunately, seems like the
keyof NodeJS.ProcessEnv
does NOT work.Do you have any idea how to solve this?
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?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
Great suggestion. That package looks great for this purpose.
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.