08/2022

Environment Variables in Node.js

When building software applications, we will have to deal with environment variables at some point.

In this note we will look into how easily we can handle environment variables (process.env) in a Node.js application.

environment-decoder.png

I don't want to be a product of my environment. I want my environment to be a product of me.

-- Frank Costello, The Departed

Environment & Variables

An environment can be defined as a group of conditions in which a system or application operates, usually an environment is named "local", "developement", "production", etc.

A group of conditions that determinates and defines this environment is called "environment variables".

An environment variable is a value that can affect the way an application runs, this value is constant at runtime and it only changes in between environments.

Some of the most common use cases are:

  • a PORT on where the application is running
  • API key to fetch data
  • credentials to connect to a database

Environment variables usually contain sensitive and personal information (fx: authentication), we always need to be cautious on how to use them.

Set environment variables

The most common way to set environment variables is with .env (dotenv) files, we will have to install the dependency in order to source the environment into our code.

We can also pass the value of the variable in the "scripts" in "package.json", we just need to be aware those values will be clear text in version control and will overwrite the value.

# .env
BASE_URL=https://marcodaniels.com
MY_ENV=true
# dev.env
BASE_URL=https://dev.marcodaniels.com
MY_ENV=false
# package.json
{
"scripts": {
"start": "MY_ENV=false run application"
}
}
if (process.env.MY_ENV!) {
// do something
}
const base = process.env.BASE_URL || ''
fetch(`${baseURL}/api`)
.then(res => res.json())

Use environment variables

All environment variables are set to the global process.env property and their values can be accessed from anywhere in our application.

We will have to make 100% sure the variables name and value are set correctly in our application in all environments, otherwise we might find some not-so-funny problems to debug.

Environment Decoder

environment-decoder is a small library that allows us to define a simple decoder and the type of the environment variables we need for our application to run.

Decoding is the process of converting code into plain text or any format that is useful for subsequent processes.

Let's have a look at it:

const myEnv = environmentDecoder({
BASE_URL: asString,
PORT: asNumber,
FEATURE_FLAG: asBoolean,
OPTIONAL_FLAG: asString.withDefault('OPTION_1')
})

In the example above, not only we're validating that all the environment variables are set, we're also making sure their types are correct.

The value myEnv.FEATURE_FLAG is a boolean for sure and there's no need to cast Boolean(myEnv.FEATURE_FLAG), for optional variables the withDefault(...) method allows us to define a fallback value at the decoder level.

Environment Decoder with TypeScript

When using TypeScript we can create a type for our decoder with DecodeType:

import {environmentDecoder, asString, DecodeType} from 'environment-decoder'
const myEnv = environmentDecoder({
BASE_URL: asString,
API_KEY: asString,
})
type EnvironmentType = DecodeType<typeof myEnv>

We can also create multiple decoders and use the DecodeType to create a function paramater:

import {environmentDecoder, asString, asBoolean, DecodeType} from 'environment-decoder'
const baseEnv = environmentDecoder({
FEATURE_FLAG: asBoolean
})
const myAPIEnv = environmentDecoder({
BASE_URL: asString,
API_KEY: asString,
})
type MyAPIType = DecodeType<typeof myAPIEnv>
const myAPIFetch = (params: MyAPIType) =>
fetch(`${params.BASE_URL}/v1/api`, {
method: 'POST',
headers: {
'X-API-KEY': params.API_KEY,
}
})
.then(response => response.json())
myAPIFetch(myAPIEnv)

This allows us to write the interface for the functions that require environment variables in a clean and safe way without the need of duplication.

Environment Decoder Exceptions

The way environment-decoder makes sure the environment variables and their types are set correctly is by throwing an exception at runtime.

This behaviour would be very similar without environment-decoder as the places where the variables are used would probably throw an exception. The main difference is that now we are able to catch all these errors in one place.

The cases where the exceptions will happen are:

  • the environment variable is not set and there is no .withDefault()
  • the environment variable value does not match the type defined (using asNumber on a value abcde)

It is recommended that we use environmentDecoder as close as possible to the start of our application. This way we can catch all erros as early as possible.

You're still here?

Thank you so much for reading this! Go ahead and check the source code for environment decoder on GitHub.

Until then!