Skip to content

Complete Guide to the Fastify Plugin System

Editor's note: This is a cross-post written by Senior Software Developer, Manuel Spigolon. Manuel has his own blog at backend.cafe where you can subscribe for updates and find more great posts. Some of the links in the article point to Manuel's personal github account.

Understanding the Fastify Plugin System

One of the most important differentiating features of Fastify is its Plugin System and the encapsulation concept. These two features are synonymous as we are talking about the same thing.

The Fastify Plugin System is a powerful tool that allows you to build a modular application. Unfortunately, it is also the most misunderstood feature of Fastify. This article will show how it works and how to avoid common mistakes. We are going to view the encapsulation with some images to make it easier to understand.

What is encapsulation?

Encapsulation is not a new engineering concept nor a Fastify exclusive feature. It is the base concept of the Fastify Plugin System, and you can't ignore it if you want to take advantage of Fastify.

You can read its formal description on the Wikipedia page .

Briefly, it is a mechanism that allows you to create an isolated  context  from another application's  contexts .

Here is a simple representation of three encapsulated contexts:

What does it mean? This concept may seem trivial, but it is very powerful:

  • Each context can have its own state.
  • Each context can't access the state of other contexts.
  • It is possible to create a context with the same code but different states.

Encapsulation and Inheritance

The encapsulation concept can be pushed to the extreme by adding the inheritance behaviour. In this case, the encapsulation is not only a mechanism to isolate the contexts, but also a mechanism to create a new context from others.

Here is a descriptive image:

This new image shows us three different  Rooted trees . It is more complex, but it is very powerful because it defines new behaviours:

  • Each context may have child contexts.
  • Each context could have at most one parent context.
  • Each context can access the state of its parent context.

That being said, we have mentioned all the concepts we need to understand the Fastify encapsulation.

Let's see how it works in Fastify.

The Fastify Plugin System demystified

In one sentence: Fastify's Plugin System is an encapsulated context builder. No more, no less.

The Plugin System has these pillars :

  1. There is only one root context, and all the plugins are children of it
  2. The parent context can't access the state of the child contexts
  3. The children's context can access the state of the parent's context

I'm used to calling the root context the  root application instance  and every child context as  plugin instance .

Since we have a root context, every Fastify application can be represented as a tree structure where every node is a context.

Let's take a look at some code to better understand how it works.

Create a new project:

text
mkdir fastify-encapsulation
cd fastify-encapsulation
npm init --yes
npm install fastify
touch app.js

In a new  app.js  file, add the following code:

js
const fastify = require('fastify')

start()

async function start() {
  const app = fastify({ logger: true })
  await app.listen(8080)
}

The  app  variable in the previous code is the  root application instance  or Fastify's root context.

Let's try to create some contexts, as shown in the previous image.

To do this, we need to call the  register  method.

Edit the  start  function as follows:

js
const app = fastify({ logger: true })

app.register(async function pluginOne (instance, opts) {
  // context 1
  instance.register(async function pluginX (instance, opts) {
  // context X
  })
})

app.register(async function pluginTwo (instance, opts) {
  // context 2
  instance.register(async function pluginY (instance, opts) {
    // context Y
    instance.register(async function pluginK (instance, opts) {
      // context K
    })
  })
  instance.register(async function pluginZ (instance, opts) {
    // context Z
  })
})

By doing this, we are creating two child contexts from the root context. Note that the first argument of the  register  method is a function that represents the  plugin instance . Its first argument, named  instance  is the  context  itself and gives us access to all of  Fastify's server methods .

We can visualize our code structure as follows, but it will look like the previous image that we recreate with the Fastify Plugin System:

As you can see, the root context is the parent of the two children contexts. This representation shows the tree structure of the application.

But, what is the difference between each context?

The encapsulated context state

As we said before, each context has its own state. In Fastify, the state is represented by:

  • The context's routes
  • The context's settings and plugins
  • The context's decorators
  • The context's hooks

This means that every time you create a new context by calling the register method, you can pass a new set of options, settings, etc. Moreover, the inheritance implementation inherits the parent context's state.

In practice, this means that the child contexts inherit the parent's hooks, decorators, settings and plugins.

js
const app = fastify({ logger: true })

// root application instance
// ❌ cannot access ctx1
// ❌ cannot access ctx2
app.decorate('rootDecorator', 42)

app.register(async function plugin (instance, opts) {
  // context 1
  // ✅ can access rootDecorator
  // ❌ cannot access ctx2
  instance.decorate('ctx1', 42)
})

app.register(async function plugin (instance, opts) {
  // context 2
  // ✅ can access rootDecorator
  // ❌ cannot access ctx1
  instance.decorate('ctx2', 42)
})

This behaviour is the brain of the Fastify Plugin System. In fact, you can play with the encapsulation to create a plugin that can be used in different contexts.

For example, we could modify to get the same result but reuse the same plugin function:

js
const app = fastify({ logger: true })

app.decorate('rootDecorator', 42)

app.register(plugin, { decoratorName: 'ctx1' })
app.register(plugin, { decoratorName: 'ctx2' })

async function plugin (instance, opts) {
  instance.decorate(opts.decoratorName, 42)
}

Here it should become clearer how encapsulation works and its benefits:

  • you can split your source code into different source files/plugins
  • load plugins with different settings (e.g.: connect to many MongoDB connections by registering the plugin twice and changing the connection URL)
  • add hooks to a limited set of routes: the ones defined in the encapsulation context

In fact, if you try to rewrite our examples by moving the  plugin  function to a file, you will verify that you can't access the JavaScript closure anymore to read some external config, but you need to rely on the Fastify  instance  argument only!

This is Fastify's secret sauce! Learn More about our commitment to Open Source

Breaking the encapsulation

You may have seen the  fastify-plugin  module that "breaks the encapsulation". But why do we need to break the encapsulation?

Let's see the previous example's structure:

If I would like to access the  ctx1  decorator from the  root  context, I can't do it, unless I break the encapsulation by wrapping the plugin function with the  fastify-plugin  module:

js
const fp = require('fastify-plugin')
const app = fastify({ logger: true })

app.decorate('rootDecorator', 42)

app.register(fp(plugin), { decoratorName: 'ctx1' })
app.register(plugin, { decoratorName: 'ctx2' })

async function plugin (instance, opts) {
  instance.decorate(opts.decoratorName, 42)
}

Now the application schema will look like this because we broke the ctx1 context, and the parent swallowed it:

Now, the  app  instance has access to the  ctx1  decorator.

Note that every plugin you will install will be wrapped with the  fastify-plugin  module, otherwise if the plugin adds a database connection, it will be encapsulated and inaccessible from the parent context!

Why is the Fastify Plugin System not easy to understand?

We have seen the Fastify Plugin System and how it works, but why is it not easy to understand?

The answer is simple: it is a powerful tool, but it is not easy to control when things become bigger.

The most common mistake is when you create a plugin hell like the following:

text
app.register(async function plugin (instance, opts) {
  instance.register(async function plugin (instance, opts) {
    instance.register(function plugin (instance, opts, next) {
      instance.register(function plugin (instance, opts, next) {
        next()
      })
      next()
    })
    instance.register(function plugin (instance, opts, next) {
      next()
    })
    instance.register(function plugin (instance, opts, next) {
      instance.register(function plugin (instance, opts, next) {
        instance.register(function plugin (instance, opts, next) {
          next()
        })
        next()
      })
      next()
    })
  })
})

The plugin hell is not wrong, but it makes the code hard to read and understand. This denotes it is less maintainable, and you may be forced to add some  global  variables to solve some issues.

To solve this problem, you can use the  fastify-overview  plugin to visualize the tree structure and be able to navigate within your code base without any difficulties.

First, we need to install the plugin and configure it:

js
async function start() {
  const app = fastify({ logger: true })
  await app.register(require('fastify-overview'))
  // ... plugin-hell ...
  await app.ready()
  const appStructure = app.overview({ hideEmpty: true })
  require('fs').writeFileSync('./appStructure.json', JSON.stringify(appStructure, null, 2))
}

The code above starts the Fastify application and exports the tree structure in a JSON file. Now, you should have a file named  appStructure.json  in your project's root directory.

The stored file can be used to visualize the tree structure of your application. You can use the  fastify-overview-ui  plugin or the  https://jsoncrack.com  website.

Copy the content of the  appStructure.json  file and paste it into the  https://jsoncrack.com  website, you will see the detailed data structure of your application, and you will be able to understand where each component is stored in your codebase.

Thanks to the plugin, you will be able to see the runtime application graph:

???? ALL the Fastify routes ???? ALL the Fastify plugins ???? ALL the Fastify decorators ???? ALL the Fastify hooks

For example, you will see any additional settings that a third-party plugin adds to your Fastify instance that you may not be aware of. More from Manuel: Upgrading Fastify’s input validation to Ajv version 8

Summary

You have read how Fastify implements encapsulation through the Plugin System and its powerful features. Moreover, you have seen how to use the fastify-overview plugin to visualize your application's tree structure to debug and explore it more easily.

If you want to learn more about the Fastify Plugin System, you can pre-order the Fastify Book which will deep dive into this topic and a lot more!

If you enjoyed this article please share and follow me on Twitter @ManuEomm !

Do you need help with Digital Product Development ? Contact us today to schedule a Discovery Workshop .

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact