Environment variables are key/value pairs that hold environment-specific values or secret values that our application needs but can't be committed to source control.
For example, to send an email, we'd need to connect to an email provider. For security reasons, we don't want that connection info committed to source control; otherwise, anyone would be able to grab it and use it for themselves at our expense.
By placing that connection info in an environment variable, we:
Keep the value a secret and out of source code
Allow ourselves to use one email provider in development and another in production
Environment Variable Values
We learned in the last lesson that our environment variables are defined within our .env file. Our project also comes with a .env.example file. The .env file holds the actual key/value pairs our application will use, while the .env.example is an example file that should just hold the keys and either empty or dummy values to help others get our project up and running.
If we open our .env, we should see something like the below.
TZ=UTC # sets default timezone
PORT=3333 # sets preferred application port
HOST=localhost # URL host
APP_URL=http://${HOST}:${PORT} # final app URL to aide linking
LOG_LEVEL=info # level to be logged
APP_KEY=ybbb8g4i... # unique secret key used for encryption
NODE_ENV=development # mode to start the server in
SESSION_DRIVER=cookie # driver to use for sessionsThis file will expand as we install additional AdonisJS packages, and we can add whatever we need for our application here as well.
Environment-Specific Variables
Sessions are how our application can maintain stateful information for a specific user, and we'll have a specific lesson discussing them a little later on. For now, all you need to know is that when running tests, we need our sessions to use a memory store instead of cookies because tests don't run in the browser and can't utilize cookies to track sessions.
By using environment variables to set the driver for our sessions, via SESSION_DRIVER we allow ourselves to easily swap this specifically for tests by creating a .env.test file.
touch .env.testCopied!
// .env.test
NODE_ENV=test
SESSION_DRIVER=memoryNow, when we run our tests, anything defined within .env.test will be used over and merge with what's defined in our .env file.
This naming convention is environment-specific, for example:
.envis the base, used in all environments.env.developmentfor development.env.stagingfor staging.env.productionfor production.env.testfor tests.env.localfor all environments except tests
Validating Environment Variables
As an additional precaution, AdonisJS includes a validation layer for our environment variables. This allows us to state that our environment variables should have a specific value or type prior to our server being able to boot.
This feature has saved me a few times as you get to working on a feature, forget you added an environment variable for it, and go to deploy only to find production doesn't yet have that environment variable. So, don't sleep on this feature!
These validations are defined within our start/env.ts file and loaded by our bin file during our application's boot. By default, this file will look something like:
/* |-------------------------------------------------------------------------- | Environment variables service |-------------------------------------------------------------------------- | | The `Env.create` method creates an instance of the Env service. The | service validates the environment variables and also cast values | to JavaScript data types. | */ import { Env } from '@adonisjs/core/env' export default await Env.create(new URL('../', import.meta.url), { NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), PORT: Env.schema.number(), APP_KEY: Env.schema.secret(), APP_URL: Env.schema.string({ format: 'url', tld: false }), HOST: Env.schema.string({ format: 'host' }), LOG_LEVEL: Env.schema.string(), /* |---------------------------------------------------------- | Variables for configuring session package |---------------------------------------------------------- */ SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const), })Copied!
- start
- env.ts
Sticking with our SESSION_DRIVER example, this states its value must specifically be cookie or memory. If it is anything else, our server will throw an exception and fail to boot, telling us exactly why. Without that step, it could be a bear to track down why our sessions aren't working.
We can specify string, number, boolean, enum, and secret value types. Strings can also specify a format, like URL, that the value must meet.
Adding an Environment Variable
We could manually add an environment variable by adding the key/value pair into our .env and a validation for it within our start/env.ts file. You might've noticed in the last lesson, though, that there is an env:add command available via the Ace CLI. Let's use that!
First, let's check out the --help info:
> $ node ace env:add --help Description: Add a new environment variable Usage: node ace env:add [options] [--] [<name>] [<value>] Arguments: [name] Variable name. Will be converted to screaming snake case [value] Variable value Options: --type[=TYPE] Type of the variable --enum-values[=ENUM-VALUES...] Allowed values for the enum type in a comma-separated list [default: ]Copied!
It accepts two arguments: a name and a value. Then, we can add a type or list of enum values for it within our env validation. At present, we don't really have a practical need to add an environment variable, so let's add an OWNER_NAME environment variable.
node ace env:add dev_name "Tom Gobich" --type=string # DONE: update .env file # DONE: update start/env.ts file # [ success ] Environment variable added successfullyCopied!
Note, in order to have our first and last name count as one argument, we need to wrap it in quotes.
Once run, we can find our new environment variable within our .env.
TZ=UTC
PORT=3333
HOST=localhost
APP_URL=http://${HOST}:${PORT}
LOG_LEVEL=info
APP_KEY=ybbb8g4i...
NODE_ENV=development
SESSION_DRIVER=cookie
++DEV_NAME=Tom GobichAgain, note that it has normalized our environment variable key to uppercase, which matches convention. Finally, we can check our start/env.ts to find:
/* |-------------------------------------------------------------------------- | Environment variables service |-------------------------------------------------------------------------- | | The `Env.create` method creates an instance of the Env service. The | service validates the environment variables and also cast values | to JavaScript data types. | */ import { Env } from '@adonisjs/core/env' export default await Env.create(new URL('../', import.meta.url), { NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), PORT: Env.schema.number(), APP_KEY: Env.schema.secret(), APP_URL: Env.schema.string({ format: 'url', tld: false }), HOST: Env.schema.string({ format: 'host' }), LOG_LEVEL: Env.schema.string(), /* |---------------------------------------------------------- | Variables for configuring session package |---------------------------------------------------------- */ SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const), DEV_NAME: Env.schema.string(), })Copied!
- start
- env.ts
Using Environment Variables
Finally, to use an environment variable, we want to import env from our start/env.ts location. This exports our environment variables wrapped in their validations, so we get intellisense and type support with it!
If we jump into our start/routes.ts file we can quickly demo an example at the top of our file.
import env from './env' console.log(`Developed by: ${env.get('DEV_NAME')}`)Copied!
- start
- routes.ts