2

I read the docs, but I still don't understand how I'm supposed to split my routes so that only some plugins apply to them.

Here's what I've got:

index.ts

import fastify from 'fastify'
import bookingsRoutes from './routes/bookings'

const server = fastify({
    logger: process.env.NODE_ENV !== 'production',
    trustProxy: '64.225.88.57', // DigitalOcean load balancer
})


server.get('/health', (_, reply) => {
    reply.send("OK")
})

server.register(bookingsRoutes)

server.listen(process.env.PORT || 3000, '0.0.0.0', (err, address) => {
    if (err) {
        server.log.error(err)
        process.exit(1)
    }
    server.log.info(`Server listening at ${address}`)
})

bookings.ts

// ...

const plugin: FastifyPluginAsync = async api => {
    await Promise.all([
        api.register(dbPlugin),
        api.register(authPlugin),
    ])
    const {db,log} = api

    api.route<{Body:BookingRequestType,Reply:BookingType}>({
        url: '/bookings',
        method: 'POST',
        async handler(req, res) {
            console.log('got user',req.user)
        }
    })
}

export default fp(plugin, '3.x')

I thought by registering dbPlugin and authPlugin inside the booking routes plugin that it would only apply to those routes, but that doesn't seem to be right. It seems to be applying to /health too.

Also not sure if I should be awaiting the 2 register functions like that but they return promises and that seems to be the only way to get the db object back out of them...

What's the proper way to do this?

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • in bookings.ts don't use `fp` -> it breaks the encapsulation as documented – Manuel Spigolon Sep 09 '21 at 12:06
  • Removing `fp` from the routes makes it work, but there still doesn't seem to be proper isoluation here. `authPlugin` depends on `dbPlugin` but if I include `dbPlugin` again in my routes it'll say "The decorator 'db' has already been added". Why isn't fastify smart enough to tell which plugin has already been included so that I can list all the dependencies explicitly? – mpen Sep 11 '21 at 00:53

1 Answers1

3

Start from this snippet:

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

fastify.register(async function plugin (privatePlugin, opts) {
  privatePlugin.addHook('onRequest', function hook (request, reply, done) {
    if (request.headers['x-auth'] === '123') {
      done()
    } else {
      done(new Error('wrong auth'))
    }
  })

  privatePlugin.get('/', async (request, reply) => {
    return { private: 'private' }
  })
})

fastify.register(async function plugin (publicPlugin, opts) {
  publicPlugin.get('/', async (request, reply) => {
    return { public: 'public' }
  })
})

fastify.listen(8080)

This shows how to create a:

  • private context and
  • a public context

all the routes registered to the privatePlugin will inherit the onRequest hook - the authentication check.

The publicPlugin will not because it is a privatePlugin's sibling. Read here for more detail

Why isn't fastify smart enough to tell which plugin has already been included so that I can list all the dependencies explicitly?

Because in Fastify you can register multiple times the same plugins with different configurations but you must check if:

  • the context has already it or not: registering twice a plugin slow down the server, consume memory etc...
  • the plugin may support multiple registrations in the same context (like fastify-mongodb does within the namespace configuration)

It is a design choice, otherwise, it would be much more confusing in complex applications.


Edit after comments:

The auth plugin should focus on authorization only and it should not care about the database.

In this case I would change your code like so:

// auth-plugin.js
const fp = require("fastify-plugin");

const plugin = async (api, options) => {
  api.addHook("preHandler", async (req, reply) => {
    if (!req.user) {
      throw new Error("Must be logged in");
    }
  });
};

module.exports = fp(plugin, {
  name: 'org-auth-plugin',
  dependencies: ['foo-db-plugin']
})

and

const fp = require("fastify-plugin");

const plugin = async (api, options) => {
  api.decorate("db", "I'm a database");
  // TODO: create/connect to database
};

module.exports = fp(plugin, {
  name: 'foo-db-plugin'
})

In this way you will be warned during the startup that your org-auth-plugin needs the db plugin to work correctly.

Manuel Spigolon
  • 11,003
  • 5
  • 50
  • 73
  • I came across that example on github, but it's missing a few things. The "privatePlugin" is both a plugin and a route handler -- I don't want to mix those. The "onRequest" hook should be extracted into its own plugin and then registered with the private route plugin only, but if you register it, it leaks into the other route handlers. – mpen Sep 13 '21 at 21:16
  • And there should be no "slow down". Maybe a tiny bootup cost, but registration should only happen once at server start if it's designed nicely. I suppose allowing multiple registrations of the same plugin makes sense in some scenarios, but then fastify should have a "one instance" option so that you can explicitly specify which plugins have which dependencies instead of expecting people to just know what plugins need to be pre-registered?? – mpen Sep 13 '21 at 21:19
  • "I don't want to mix those" --> you are not using the Fastify's plugins system in this way and you are trying to create a single context with all your application. You can create as many modules as you need. "it leaks into the other route handlers" --> if you register the plugin in the `privatePlugin` the hook remains in that context. I think you refers to `plugin` as an add-on: but in fastify everything is a plugin – Manuel Spigolon Sep 13 '21 at 23:15
  • "registration should only happen once" --> it is. but for example, I register two mongodb plugins to double the connection pool and using different users with different grants. "one instance" ---> The plugin system needs the developers to change their mindset compared to the usual express application. Every plugin should be autoconsistent – Manuel Spigolon Sep 13 '21 at 23:19
  • Maybe a fuller example will help explain the problem: https://codesandbox.io/s/throbbing-glade-9fs7y?file=/src/auth-plugin.js `auth-plugin` depends on `db-plugin` and `private-routes` depends on `db-plugin` *and* `auth-plugin`. Now because `auth-plugin` already included `db-plugin`, `private-routes` will crash because `db-plugin` is being registered twice. Easy solution is to remove `db-plugin` from `private-routes` because it's indirectly included via `auth-plugin`, but then there's a hidden dependency – mpen Sep 14 '21 at 01:51
  • Sorry, you're right. Auth plugin shouldn't depend on db -- that was my mistake. User plugin does. And then auth plugin depends on user plugin. I'm trying with `dependencies` now, that does work better but still feels a little weird to me. If the routes don't care about `user` but need auth, for example, why should the routes have to know about/register user-plugin? Why can't the auth plugin use a default implementation of user plugin? – mpen Sep 15 '21 at 01:51