The hitchhiker's guide to plugins
First of all, DON'T PANIC!
Fastify was built from the beginning to be an extremely modular system. We built a powerful API that allows you to add methods and utilities to Fastify by creating a namespace. We built a system that creates an encapsulation model, which allows you to split your application into multiple microservices at any moment, without the need to refactor the entire application.
Table of contents
Register
As with JavaScript, where everything is an object, in Fastify everything is a plugin.
Your routes, your utilities, and so on are all plugins. To add a new plugin,
whatever its functionality may be, in Fastify you have a nice and unique API:
register.
fastify.register(
require('./my-plugin'),
{ options }
)
register creates a new Fastify context, which means that if you perform any
changes on the Fastify instance, those changes will not be reflected in the
context's ancestors. In other words, encapsulation!
Why is encapsulation important?
Well, let's say you are creating a new disruptive startup, what do you do? You create an API server with all your stuff, everything in the same place, a monolith!
Ok, you are growing very fast and you want to change your architecture and try microservices. Usually, this implies a huge amount of work, because of cross dependencies and a lack of separation of concerns in the codebase.
Fastify helps you in that regard. Thanks to the encapsulation model, it will completely avoid cross dependencies and will help you structure your code into cohesive blocks.
Let's return to how to correctly use register.
As you probably know, the required plugins must expose a single function with the following signature
module.exports = function (fastify, options, done) {}
Where fastify is the encapsulated Fastify instance, options is the options
object, and done is the function you must call when your plugin is ready.
Fastify's plugin model is fully reentrant and graph-based, it handles
asynchronous code without any problems and it enforces both the load and close
order of plugins. How? Glad you asked, check out
avvio! Fastify starts loading the plugin
after .listen(), .inject() or .ready() are called.
Inside a plugin you can do whatever you want, register routes, utilities (we
will see this in a moment) and do nested registers, just remember to call done
when everything is set up!
module.exports = function (fastify, options, done) {
fastify.get('/plugin', (request, reply) => {
reply.send({ hello: 'world' })
})
done()
}
Well, now you know how to use the register API and how it works, but how do we
add new functionality to Fastify and even better, share them with other
developers?
Decorators
Okay, let's say that you wrote a utility that is so good that you decided to make it available along with all your code. How would you do it? Probably something like the following:
// your-awesome-utility.js
module.exports = function (a, b) {
return a + b
}
const util = require('./your-awesome-utility')
console.log(util('that is ', 'awesome'))
Now you will import your utility in every file you need it in. (And do not forget that you will probably also need it in your tests).
Fastify offers you a more elegant and comfortable way to do this, decorators.
Creating a decorator is extremely easy, just use the
decorate API:
fastify.decorate('util', (a, b) => a + b)
Now you can access your utility just by calling fastify.util whenever you need
it - even inside your test.
And here starts the magic; do you remember how just now we were talking about
encapsulation? Well, using register and decorate in conjunction enable
exactly that, let me show you an example to clarify this:
fastify.register((instance, opts, done) => {
instance.decorate('util', (a, b) => a + b)
console.log(instance.util('that is ', 'awesome'))
done()
})
fastify.register((instance, opts, done) => {
console.log(instance.util('that is ', 'awesome')) // This will throw an error
done()
})
Inside the second register call instance.util will throw an error because
util exists only inside the first register context.
Let's step back for a moment and dig deeper into this: every time you use the
register API, a new context is created which avoids the negative situations
mentioned above.
Do note that encapsulation applies to the ancestors and siblings, but not the children.
fastify.register((instance, opts, done) => {
instance.decorate('util', (a, b) => a + b)
console.log(instance.util('that is ', 'awesome'))
fastify.register((instance, opts, done) => {
console.log(instance.util('that is ', 'awesome')) // This will not throw an error
done()
})
done()
})
fastify.register((instance, opts, done) => {
console.log(instance.util('that is ', 'awesome')) // This will throw an error
done()
})
Take home message: if you need a utility that is available in every part of
your application, take care that it is declared in the root scope of your
application. If that is not an option, you can use the fastify-plugin utility
as described here.
decorate is not the only API that you can use to extend the server
functionality, you can also use decorateRequest and decorateReply.
decorateRequest and decorateReply? Why do we need them if we already have
decorate?
Good question, we added them to make Fastify more developer-friendly. Let's see an example:
fastify.decorate('html', payload => {
return generateHtml(payload)
})
fastify.get('/html', (request, reply) => {
reply
.type('text/html')
.send(fastify.html({ hello: 'world' }))
})
It works, but it could be much better!
fastify.decorateReply('html', function (payload) {
this.type('text/html') // This is the 'Reply' object
this.send(generateHtml(payload))
})
fastify.get('/html', (request, reply) => {
reply.html({ hello: 'world' })
})
Reminder that the this keyword is not available on arrow functions,
so when passing functions in decorateReply and decorateRequest as
a utility that also needs access to the request and reply instance,
a function that is defined using the function keyword is needed instead
of an arrow function expression.
In the same way you can do this for the request object:
fastify.decorate('getHeader', (req, header) => {
return req.headers[header]
})
fastify.addHook('preHandler', (request, reply, done) => {
request.isHappy = fastify.getHeader(request.raw, 'happy')
done()
})
fastify.get('/happiness', (request, reply) => {
reply.send({ happy: request.isHappy })
})
Again, it works, but it can be much better!
fastify.decorateRequest('setHeader', function (header) {
this.isHappy = this.headers[header]
})
fastify.decorateRequest('isHappy', false) // This will be added to the Request object prototype, yay speed!
fastify.addHook('preHandler', (request, reply, done) => {
request.setHeader('happy')
done()
})
fastify.get('/happiness', (request, reply) => {
reply.send({ happy: request.isHappy })
})
We have seen how to extend server functionality and how to handle the encapsulation system, but what if you need to add a function that must be executed whenever the server "emits" an event?
Hooks
You just built an amazing utility, but now you need to execute that for every request, this is what you will likely do:
fastify.decorate('util', (request, key, value) => { request[key] = value })
fastify.get('/plugin1', (request, reply) => {
fastify.util(request, 'timestamp', new Date())
reply.send(request)
})
fastify.get('/plugin2', (request, reply) => {
fastify.util(request, 'timestamp', new Date())
reply.send(request)
})
I think we all agree that this is terrible. Repeated code, awful readability and it cannot scale.
So what can you do to avoid this annoying issue? Yes, you are right, use a hook!
fastify.decorate('util', (request, key, value) => { request[key] = value })
fastify.addHook('preHandler', (request, reply, done) => {
fastify.util(request, 'timestamp', new Date())
done()
})
fastify.get('/plugin1', (request, reply) => {
reply.send(request)
})
fastify.get('/plugin2', (request, reply) => {
reply.send(request)
})
Now for every request, you will run your utility. You can register as many hooks as you need.
Sometimes you want a hook that should be executed for just a subset of routes, how can you do that? Yep, encapsulation!
fastify.register((instance, opts, done) => {
instance.decorate('util', (request, key, value) => { request[key] = value })
instance.addHook('preHandler', (request, reply, done) => {
instance.util(request, 'timestamp', new Date())
done()
})
instance.get('/plugin1', (request, reply) => {
reply.send(request)
})
done()
})
fastify.get('/plugin2', (request, reply) => {
reply.send(request)
})
Now your hook will run just for the first route!
An alternative approach is to make use of the onRoute hook to customize application routes dynamically from inside the plugin. Every time a new route is registered, you can read and modify the route options. For example, based on a route config option:
fastify.register((instance, opts, done) => {
instance.decorate('util', (request, key, value) => { request[key] = value })
function handler(request, reply, done) {
instance.util(request, 'timestamp', new Date())
done()
}
instance.addHook('onRoute', (routeOptions) => {
if (routeOptions.config && routeOptions.config.useUtil === true) {
// set or add our handler to the route preHandler hook
if (!routeOptions.preHandler) {
routeOptions.preHandler = [handler]
return
}
if (Array.isArray(routeOptions.preHandler)) {
routeOptions.preHandler.push(handler)
return
}
routeOptions.preHandler = [routeOptions.preHandler, handler]
}
})
fastify.get('/plugin1', {config: {useUtil: true}}, (request, reply) => {
reply.send(request)
})
fastify.get('/plugin2', (request, reply) => {
reply.send(request)
})
done()
})
This variant becomes extremely useful if you plan to distribute your plugin, as described in the next section.
As you probably noticed by now, request and reply are not the standard
Node.js request and response objects, but Fastify's objects.