How to Access dotenv Variables Using fastify-env Plugin

As a small project to upgrade my web developer portfolio, I decided to take a JavaScript trivia game I coded for a class project and upgrade it from a simple front end only application to a full stack web application. Initially I was going to use Mongo, Express, Vue, and Node as the stack. However, after poking around the web and reading up on Express alternatives, I thought it would be fun to try out a new framework that supported async/await and decided on fastify.

The Problem: How do I Access my .env File in Fastify?

Coming from an Express background, I was used to using the dotenv module to read configuration variables such as the username and password for the database, secrets for jsonwebtokens, and others from my .env file. Getting onboard with fastify's plugin ecosystem, I installed fastify-env and attempted to use it to load the contents of my .env file. An initial challenge I faced was the documentation for accessing .env variables using fastify-env seemed a bit sparse and I was unable to find any good tutorials.

After trying several different approaches with fastify-env and failing to read the variables from .env, I gave up and installed dotenv. This approach worked and I was able to successfully connect fastify with the Mongo database. However, one huge drawback with using dotenv with fastify is that the .env variables are not available on the fastify instance, so accessing them from modules buried in the directory structure rapidly becomes a headache.

I will be using jsonwebtokens in my application to authenticate users on the backend. To validate them, I need to store a secret on the server and access it from different modules that include the validation logic. While the dotenv solution worked well enough for the database credentials, it was too unwieldy for accessing the secret. So, I gave fastify-env a second try.

The Solution, or the Key Points I Missed

Using fastify for the first time, I was learning several new concepts at once while getting the back end up and running and missed several critical items in my initial attempts to use fastify-env. Hopefully the following summary of my experience will help others new to fastify-env save some time and frustration.

.env Variables Need to be Included in the Schema

The first thing I missed in my first attempt at using fastify-env was that the variables in the .env file need to be included in the schema used by fastify-env, otherwise they will not be accessible. The following code snippets give an example of how this works:

.env

USERNAME=databaseUsername
PASSWORD=doubleSecretDatabasePassword

server.js

const schema = {
  type: 'object',
  required: ['PASSWORD', 'USERNAME'],
  properties: {
    PASSWORD: {
      type: 'string'
    },
    USERNAME: {
      type: 'string'
    }
  }
}

Set the "data" key to "process.env"

The second key point I missed was that the data key in the options object needs to be set to "process.env" to read the .env file. Simply setting the dotenv key to true is not enough. The following code snippet shows how to correctly set both keys.

server.js

const options = {
  dotenv: true,
  data: process.env
}

How I Thought ready() Worked Versus How it Really Works

The third and final thing I didn't realize when initially attempting to use fastify-env was that awaiting fastify.ready() before fastify.listen() does not load all the plugins in order. However awaiting fastify.after() on the line after fastify.register() will ensure the .env variables are defined following fastify.after(), as shown in the following code snippet.

server.js

fastify.register(fastifyEnv, options)
await fastify.after()

// Now the .env variables are defined

Putting it all Together

The following code snippet shows my entire solution using fastify-env to set up a connection url to authenticate to a MongoDB database using username and password values set in a .env file.

server.js

// Fastify
const fastify = require('fastify')({
  logger: true
})

const fastifyEnv = require('fastify-env')
const schema = {
  type: 'object',
  required: ['DB_PASSWORD', 'DB_USERNAME'],
  properties: {
    DB_PASSWORD: {
      type: 'string'
    },
    DB_USERNAME: {
      type: 'string'
    }
  }
}

const options = {
  confKey: 'config',
  schema,
  dotenv: true,
  data: process.env
}

const initialize = async () => {
  fastify.register(fastifyEnv, options)
  await fastify.after()

  // Database
  // Connection URL
  const username = encodeURIComponent(fastify.config.DB_USERNAME)
  const password = encodeURIComponent(fastify.config.DB_PASSWORD)
  const dbName = 'databaseName'

  const url = `mongodb://${username}:${password}@localhost:27017/${dbName}`

  fastify.register(require('./database-connector'), {
    url,
    useUnifiedTopology: true
  })
}

initialize()

// Fire up the server
(async () => {
  try {
    await fastify.ready()
    await fastify.listen(process.env.PORT)
  } catch (error) {
    fastify.log.error(error)
    process.exit(1)
  }
})()

I hope other coders find this useful. Also, if any fastify-env experts have suggestions for improving this approach, please feel free to leave them in the comments. Thanks for reading and happy coding!

Please note: "database-connection" is a fastify plugin I wrote to use the official MongoDB driver version 4.x because fastify-mongodb was using the 3.x driver under the hood at that time. Since then, fastify-mongodb has been updated to use the 4.x driver, so probably use that in your project.

20