Featurevisor

Framework guides

Fastify

A Fastify process keeps one Featurevisor instance for every request, and each request evaluates with its own context.

Install

In an existing Fastify application:

Command
npm install @featurevisor/sdk

Save the refresh helper from the Node.js guide as datafile-refresh.mjs, next to server.mjs. You need only the helper, not the Node.js startup code.

The server

This server loads the datafile before it listens, refreshes it every five minutes, and closes cleanly on SIGTERM. It expects an authentication hook, registered before the routes in the same scope, that sets request.user from a verified session:

server.mjs
import Fastify from 'fastify'
import { createFeaturevisor } from '@featurevisor/sdk'
import { refreshDatafile, waitForRefresh } from './datafile-refresh.mjs'
const DATAFILE_URL = 'https://cdn.example.com/featurevisor-all.json'
const fastify = Fastify({ logger: true })
const f = createFeaturevisor({})
// Register your authentication hook here, before the routes
fastify.get('/checkout', async (request, reply) => {
if (!request.user) return reply.code(401).send()
const context = {
userId: String(request.user.id),
country: request.user.country,
}
return f.isEnabled('checkout', context) ? 'New checkout' : 'Checkout'
})
let refreshTimer
fastify.addHook('onClose', async () => {
clearInterval(refreshTimer)
await waitForRefresh(f)
await f.close()
})
const start = async () => {
await refreshDatafile(f, DATAFILE_URL)
await fastify.listen({ port: 3000 })
refreshTimer = setInterval(() => {
refreshDatafile(f, DATAFILE_URL).catch((error) =>
fastify.log.error(error),
)
}, 5 * 60 * 1000)
refreshTimer.unref()
const shutdown = () => {
clearInterval(refreshTimer)
fastify.close().catch(reportError)
}
process.once('SIGTERM', shutdown)
process.once('SIGINT', shutdown)
}
function reportError(error) {
fastify.log.error(error)
process.exitCode = 1
}
start()
.catch(async (error) => {
reportError(error)
await fastify.close()
})
.catch(reportError)

Run it with node server.mjs. It needs a Node.js version with fetch and AbortSignal.timeout.

  • If the first datafile cannot be loaded, the server does not start.
  • A failed refresh keeps the previous datafile.
  • On shutdown, Fastify lets the current requests finish, then onClose waits for any refresh in progress and closes the instance.
  • If your application already handles signals, add these steps to its handler instead. Never add a second refresh loop, and never call setContext() on the shared instance for one user.

A child instance per request

When routes in several modules evaluate for the same user, give each request a child instance with that user's context. Fastify's request decorators cannot start with an object, so declare the property as null and set it in a hook after authentication:

server.mjs
fastify.decorateRequest('features', null)
fastify.addHook('preHandler', async (request) => {
if (request.user) {
request.features = f.spawn({
userId: String(request.user.id),
country: request.user.country,
})
}
})
async function closeFeatures(request) {
request.features?.close()
request.features = null
}
fastify.addHook('onResponse', closeFeatures)
fastify.addHook('onRequestAbort', closeFeatures)

A route then reads values without building the context again:

fastify.get('/basket', async (request, reply) => {
if (!request.features) return reply.code(401).send()
return {
newCheckout: request.features.isEnabled('checkout'),
buyLabel: request.features.getVariable('buyButton', 'label'),
}
})

For user-4 in the Netherlands, this returns newCheckout: true and buyLabel: 'Nu kopen'. The child follows the parent's datafile updates. onResponse does not run when the client aborts, so onRequestAbort releases the child in that case.

TypeScript

Declare the property on FastifyRequest, in a custom.d.ts listed in tsconfig.json:

custom.d.ts
import type { FeaturevisorChildInstance } from '@featurevisor/sdk'
import 'fastify'
declare module 'fastify' {
interface FastifyRequest {
features: FeaturevisorChildInstance | null
}
}

Your authentication library declares user the same way.

Previous
Express.js