Environment variables

Environment variables

Environment variables serve the purpose of storing secrets like the database password, the app secret, or an API key outside of your application codebase.

Also, environment variables can be used to have different configurations for different environments. For example, you may use a memory mailer during tests, an SMTP mailer during development, and a third-party service in production.

Since environment variables are supported by all operating systems, deployment platforms, and CI/CD pipelines, they have become a de-facto standard for storing secrets and environment-specific config.

In this guide, we will learn how to leverage environment variables inside an AdonisJS application.

Reading environment variables

Node.js natively exposes all the environment variables as an object through the process.env global property, and you may access them as follows.

process.env.NODE_ENV
process.env.HOST
process.env.PORT

Using the AdonisJS env module

Reading environment variables via the process.env object requires no setup on the AdonisJS side, as the Node.js runtime supports it. However, in the rest of this document, we will use the AdonisJS env module for the following reasons.

  • Ability to store and parse environment variables from multiple .env files.
  • Validate environment variables as soon as the application starts.
  • Have static-type safety for validated environment variables.

The env module is instantiated inside the start/env.ts file, and you may access it elsewhere inside your application as follows.

import env from '#start/env'
env.get('NODE_ENV')
env.get('HOST')
env.get('PORT')
// Returns 3333 when PORT is undefined
env.get('PORT', 3333)

Sharing env module with Edge templates

If you want to access environment variables within edge templates, then you must share the env module as a global variable with edge templates.

You can create view.ts as a preload file inside the start directory and write the following lines of code inside it.

start/view.ts
import env from '#start/env'
import edge from 'edge.js'
edge.global('env', env)

Validating environment variables

The validation rules for environment variables are defined inside the start/env.ts file using the Env.create method.

The validation is performed automatically when you first import this file. Typically, the start/env.ts file is imported by one of the config files in your project. If not, then AdonisJS will import this file implicitly before booting the application.

The Env.create method accepts the validation schema as a key-value pair.

  • The key is the name of the environment variable.
  • The value is the function that performs the validation. It can be a custom inline function or a reference to pre-defined schema methods like schema.string or schema.number.
import Env from '@adonisjs/core/env'
/**
* App root is used to locate .env files inside
* the project root.
*/
const APP_ROOT = new URL('../', import.meta.url)
export default await Env.create(APP_ROOT, {
HOST: Env.schema.string({ format: 'host' }),
PORT: Env.schema.number(),
APP_KEY: Env.schema.string(),
APP_NAME: Env.schema.string(),
CACHE_VIEWS: Env.schema.boolean(),
SESSION_DRIVER: Env.schema.string(),
NODE_ENV: Env.schema.enum([
'development',
'production',
'test'
] as const),
})

Static-type information

The same validation rules are used to infer the static-type information. The type information is available when using the env module.

Validator schema API

schema.string

The schema.string method ensures the value is a valid string. Empty strings fail the validation, and you must use the optional variant to allow empty strings.

{
APP_KEY: Env.schema.string()
}
// Mark APP_KEY to be optional
{
APP_KEY: Env.schema.string.optional()
}

The string value can be validated for its formatting. Following is the list of available formats.

host

Validate the value to be a valid URL or an IP address.

{
HOST: Env.schema.string({ format: 'host' })
}

url

Validate the value to be a valid URL. Optionally, you can make the validation less strict by allowing URLs not to have protocol or tld.

{
S3_ENDPOINT: Env.schema.string({ format: 'url' })
// Allow URLs without protocol
S3_ENDPOINT: Env.schema.string({ format: 'url', protocol: false })
// Allow URLs without tld
S3_ENDPOINT: Env.schema.string({ format: 'url', tld: false })
}

email

Validate the value to be a valid email address.

{
SENDER_EMAIL: Env.schema.string({ format: 'email' })
}

schema.boolean

The schema.boolean method ensures the value is a valid boolean. Empty values fail the validation, and you must use the optional variant to allow empty values.

The string representations of 'true', '1', 'false', and '0' are cast to the boolean data type.

{
CACHE_VIEWS: Env.schema.boolean()
}
// Mark it as optional
{
CACHE_VIEWS: Env.schema.boolean.optional()
}

schema.number

The schema.number method ensures the value is a valid number. The string representation of a number value is cast to the number data type.

{
PORT: Env.schema.number()
}
// Mark it as optional
{
PORT: Env.schema.number.optional()
}

schema.enum

The schema.enum method validates the environment variable against one of the pre-defined values. The enum options can be specified as an array of values or a TypeScript native enum type.

{
NODE_ENV: Env
.schema
.enum(['development', 'production'] as const)
}
// Mark it as optional
{
NODE_ENV: Env
.schema
.enum
.optional(['development', 'production'] as const)
}
// Using native enums
enum NODE_ENV = {
development = 'development',
production = 'production'
}
{
NODE_ENV: Env.schema.enum(NODE_ENV)
}

Custom functions

Custom functions can perform validations not covered by the schema API.

The function receives the name of the environment variable as the first argument and the value as the second argument. It must return the final value post-validation.

{
PORT: (name, value) => {
if (!value) {
throw new Error('Value for PORT is required')
}
if (isNaN(Number(value))) {
throw new Error('Value for PORT must be a valid number')
}
return Number(value)
}
}

Defining environment variables

In development

The environment variables are defined inside the .env file during development. The env module looks for this file within the project's root and automatically parses it (if it exists).

.env
PORT=3333
HOST=0.0.0.0
NODE_ENV=development
APP_KEY=sH2k88gojcp3PdAJiGDxof54kjtTXa3g
SESSION_DRIVER=cookie
CACHE_VIEWS=false

In production

Using your deployment platform to define the environment variables is recommended in production. Most modern-day deployment platforms have first-class support for defining environment variables from their web UI.

Suppose your deployment platform provides no means for defining environment variables. You can create a .env file in the project root or at some different location on your production server.

AdonisJS will automatically read the .env file from the project root. However, you must set the ENV_PATH variable when the .env file is stored at some different location.

# Attempts to read .env file from project root
node server.js
# Reads the .env file from the "/etc/secrets" directory
ENV_PATH=/etc/secrets node server.js

During tests

The environment variables specific to the test environment must be defined within the .env.test file. The values from this file override the values from the .env file.

.env
NODE_ENV=development
SESSION_DRIVER=cookie
ASSETS_DRIVER=vite
.env.test
NODE_ENV=test
SESSION_DRIVER=memory
ASSETS_DRIVER=fake
// During tests
import env from '#start/env'
env.get('SESSION_DRIVER') // memory

All other dot-env files

Alongside the .env file, AdonisJS processes the environment variables from the following dot-env files. Therefore, you can optionally create these files (if needed).

The file with the top-most rank overrides the values from the bottom rank files.

Rank Filename Notes
1st .env.[NODE_ENV].local Loaded for the current NODE_ENV. For example, if the NODE_ENV is set to development, then the .env.development.local file will be loaded.
2nd .env.local Loaded in all the environments except the test and testing environments
3rd .env.[NODE_ENV] Loaded for the current NODE_ENV. For example, if the NODE_ENV is set to development, then the .env.development file will be loaded.
4th .env Loaded in all the environments. You should add this file to .gitignore when storing secrets inside it.

Using variables inside the dot-env files

Within dot-env files, you can reference other environment variables using the variable substitution syntax.

We compute the APP_URL from the HOST and the PORT properties in the following example.

HOST=localhost
PORT=3333
URL=$HOST:$PORT

All letter, numbers, and the underscore (_) after the $ sign are used to form a variable name. You must wrap the variable name inside curly braces {} if the name has special characters other than an underscore.

REDIS-USER=admin
REDIS-URL=localhost@${REDIS-USER}

Escaping the $ sign

To use the $ sign as a value, you must escape it to prevent variable substitution.

PASSWORD=pa\$\$word