Manage your Node.js .env file with Envalid : A clean way to access your secrets keys in prod ⚡️
1. What is a .env file?
A .env or dotenv file of an app is a text file that contains environment-specific configurations. Most of the time between Local, Staging, and Production environments, your app will need to behave visually the same but behind the scenes, each of these environments can be configured with different keys, constants, API Keys, etc.
2. what a .env file looks like
As I previously said, a .env file is a text file, containing lines of declaration where each declaration represent a single variable. It is conventionally advised to set each variable name inside the file with uppercase words separated by an underscore ( _ ), follow with =
(without space) by the value. Here is an example:
VARIABLE_NAME=value
3. Access it in a Node.js app
In Node.Js, there is an object called process
which is a global
that provides information about, and control over, the current Node.js process. As a global, it is always available to Node.js applications without using require()
or import
. So in Node.js, you will need to write process.env.VARIABLE_NAME
to access the value set in the .env file.
So is it possible to access configurations variables with the process
object, what is the purpose of ENVALID
? 🤔
According to Envalid README file, Envalid
is a small library for validating and accessing environment variables in Node.js (v8.12 or later) programs, aiming to:
- ensure that your program only runs when all of its environment dependencies are met
- give you executable documentation about the environment your program expects to run in
- give you an immutable API for your environment variables, so they don't change from under you while the program is running.
4. Setup 🏄🏻♂️
In this post, i will use typescript with this open-source node-typescript-starter project to show you how it works.
- Open your favorite terminal and clone the starter template with
git clone
https://github.com/jsynowiec/node-typescript-boilerplate.g
it your_project_name
andcd your_project_name
- Install node dependencies with
npm install
. If you are usingYARN
as a package manager, you will need to delete thepackage-lock.json
at the root folder to avoid conflict with yarn's-lock.json
file. - Delete the following block inside the package.json (starting at line 5) file to remove targeting a specific node.js version. In this post, we will just focus on looking at how Envalid works.
"engines": {
"node": ">= 16.13 <17"
},
- We will also need to install, a useful package that will restart the server each time a file changes and
ts-node
a typescript execution engine for node. Sonpm install --save-dev nodemon ts-node
- replace
start
script inside package.json with the following to add nodemon.
"start": "npm run build && nodemon build/src/main.js",
- then in the terminal,
npm start
You should be able to see the output.
Now call the default greeter(name:string)
a function that returns a Promise with
greeter('James Bond').then((result) => console.log(result));
this will automatically trigger a build and print Hello, James Bond
in the terminal
[nodemon] clean exit - waiting for changes before restart
[nodemon] restarting due to changes...
[nodemon] starting `ts-node src/main.ts`
[nodemon] restarting due to changes...
[nodemon] starting `ts-node src/main.ts`
Hello, James Bond
[nodemon] clean exit - waiting for changes before restart
5. Adding .env 🤾🏼♂️
For this app, we will implement something very simple. The app will be a simple one with the main function that displays in the console the redirection link corresponding to a specific user of an array according to its ROLE
if we
create a .env file at the root of the folder and add it to .gitignore
file. For this post, let's consider our app will store 3 variables, that are:
ADMIN_REDIRECT_URL=/v1/custom/path/admin/login
which is where to redirect the user if it's an admin.DEFAULT_REDIRECT_URL=/v1/custom/path/user/login
which is the default redirect url for usersDASHBORD_BASE_URL=https://your_appname.com
Your .env file should look like this
ADMIN_REDIRECT_URL=/v1/custom/path/admin/login
DEFAULT_REDIRECT_URL=/v1/custom/path/user/login
DASHBORD_BASE_URL=https://your_appname.com # REPLACE THIS WITH ANY URL
Now let's access these variables in our app.
install Envalid with npm install envalid
or yarn add envalid
if you are using yarn.
create a new file env.ts
inside the src
folder and add the following code.
import { cleanEnv, str } from 'envalid';
const env = cleanEnv(process.env, {
ADMIN_REDIRECT_URL: str({ default: 'any_default_url' }),
DEFAULT_REDIRECT_URL: str({ default: 'any_default_url' }),
DASHBORD_BASE_URL: str({ default: 'any_default_url' }),
});
export default env;
replace the code inside main.ts
file with the following :
import env from './env';
enum UserType {
admin,
customer,
}
interface User {
name: string;
role: UserType;
}
const users = [
{ name: 'John do', role: UserType.admin },
{ name: 'Marc Vince', role: UserType.customer },
{ name: 'Stephen Huke', role: UserType.admin },
];
const printHello = (user: User): string => {
const { name, role } = user;
if (role === UserType.admin) {
return `Hello ${name}! , Your login URL is: ${env.DASHBORD_BASE_URL}${env.ADMIN_REDIRECT_URL}`;
}
return `Hello ${name}! , Your login URL is: ${env.DASHBORD_BASE_URL}${env.DEFAULT_REDIRECT_URL}`;
};
const start = () => {
users.forEach((user) => {
console.log(printHello(user));
});
};
start();
This will print the following in the console :
[nodemon] starting `ts-node src/main.ts`
Hello John do! , Your login URL is: https://your_appname.com/v1/custom/path/admin/login
Hello Marc Vince! , Your login URL is: https://your_appname.com/v1/custom/path/user/login
Hello Stephen Huke! , Your login URL is: https://your_appname.com/v1/custom/path/admin/login
You can see that the .env variables have been well displayed.
6. Envalid API
if you look at the env.ts
file, you will notice the import of cleanEnv()
to load environment variables. cleanEnv()
returns a sanitized, immutable environment object, and accepts three positional arguments:
environment
- An object containing your env vars (eg.process.env
)validators
- An object that specifies the format of required vars.options
- An (optional) object, which supports the following key:reporter
- Pass in a function to override the default error handling and console output.
By using cleanEnv()
, it will log an error message and exit (in Node) or throw (in browser) if any required env vars are missing or invalid. You can override this behavior by writing your own reporter.
7. Validator types
Node's process.env
only stores strings, but sometimes you want to retrieve other types (booleans, numbers), or validate that an env var is in a specific format (JSON, URL, email address). To this end, the following validation functions are available:
str()
- Passes string values through, will ensure a value is present unless adefault
value is given. Note that an empty string is considered a valid value - if this is undesirable you can easily create your own validator (see below)bool()
- Parses env var strings"1", "0", "true", "false", "t", "f"
into booleansnum()
- Parses an env var (eg."42", "0.23", "1e5"
) into a Numberemail()
- Ensures an env var is an email addresshost()
- Ensures an env var is either a domain name or an ip address (v4 or v6)port()
- Ensures an env var is a TCP port (1-65535)url()
- Ensures an env var is a URL with a protocol and hostnamejson()
- Parses an env var withJSON.parse
Each validation function accepts an (optional) object with the following attributes:
choices
- An Array that lists the admissible parsed values for the env var.default
- A fallback value, which will be present in the output if the env var wasn't specified. Providing a default effectively makes the env var optional. Note thatdefault
values are not passed through validation logic.devDefault
- A fallback value to use only whenNODE_ENV
is not'production'
. This is handy for env vars that are required for production environments but optional for development and testing.desc
- A string that describes the env var.example
- An example value for the env var.docs
- A url that leads to more detailed documentation about the env var.
8. Custom validators
You can easily create your own validator functions with envalid.makeValidator()
. It takes a function as its only parameter, and should either return a cleaned value or throw if the input is unacceptable:
import { makeValidator, cleanEnv } from 'envalid'
const twochars = makeValidator(x => {
if (/^[A-Za-z]{2}$/.test(x)) return x.toUpperCase()
else throw new Error('Expected two letters')
})
const env = cleanEnv(process.env, {
INITIALS: twochars()
});
9. Error Reporting
By default, if any required environment variables are missing or have invalid values, Envalid will log a message and call process.exit(1)
. You can override this behavior by passing in your own function as options.reporter
. For example:
const env = cleanEnv(process.env, myValidators, {
reporter: ({ errors, env }) => {
emailSiteAdmins('Invalid env vars: ' + Object.keys(errors))
}
})
Additionally, Envalid exposes EnvError
and EnvMissingError
, which can be checked in case specific error handling is desired:
const env = cleanEnv(process.env, myValidators, {
reporter: ({ errors, env }) => {
for (const [envVar, err] of Object.entries(errors)) {
if (err instanceof envalid.EnvError) {
...
} else if (err instanceof envalid.EnvMissingError) {
...
} else {
...
}
}
}
})
Thanks for reading, the source code is available here.
Follow me on social media to stay up to date with the latest posts.