Fastify

This guide is for a Fastify 4 or 5 server on Node.js 18 or later. A feature flag is a setting in your code you can turn on, off, or change without a new deploy. By the end, any route can read one from request.avsb. You get this by installing two packages, @avsbhq/node and the @avsbhq/utils/middleware/fastify plugin.

1

Install

Install @avsbhq/node and @avsbhq/utils.

2

Get your SDK key

Open Environments in your project's sidebar and copy the SDK key.

3

Set up the server SDK

Create one AvsbServer when your app starts. Wait for onReady() before you handle any requests.

4

Register the Fastify plugin

Register the plugin before any routes that need flags. It adds request.avsb to every request that comes in after it.

5

Read a flag in a route

Read flags from request.avsb inside any route that runs after the plugin.

6

Track an event

Call request.avsb.track to record a conversion for the current user.

7

Share a client without passing it around

Deep in your code, call getRequestClient to get the current user's client. You never have to pass request down through every function. Fastify has no built-in way to do this, so the plugin uses a Node.js feature called AsyncLocalStorage to make it work.

Install the packages:

Shell
npm install @avsbhq/node@^1 @avsbhq/utils@^1
Shell1 line

Copy the SDK key into your environment file (.env):

Shell
AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
Shell1 line
Environments is its own item in the sidebar. Click Reveal, then Copy, to get the SDK key for this environment.
  1. Environments lives in the sidebar on its own, not inside Settings.
  2. Click Reveal to see the full key, then Copy to copy it.

Set up the server SDK (src/avsb.ts):

TypeScript
import { AvsbServer } from '@avsbhq/node'export const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })export async function waitForAvsb(): Promise<void> {  const result = await avsb.onReady()  if (!result.success && !result.degraded) {    console.warn('[avsb] SDK init degraded: serving defaults', result.error)  }}
TypeScript10 lines

Register the Fastify plugin before any routes that need flags (src/server.ts):

TypeScript
import Fastify from 'fastify'import { fastifyPlugin } from '@avsbhq/utils/middleware/fastify'import type { FastifyPluginOptions } from '@avsbhq/utils/middleware/fastify'import { avsb, waitForAvsb } from './avsb'const fastify = Fastify({ logger: true })fastify.register(fastifyPlugin, {  server: avsb,  contextFrom: (request) => {    // `contextFrom` receives the raw request, so narrow what you read off it.    const headers = request.headers as Record<string, string | undefined>    const uid = headers['x-user-id']    if (!uid) return undefined    return { kind: 'user', key: uid }  },  withDecisionLog: true,} satisfies FastifyPluginOptions)fastify.register(import('./routes/checkout'), { prefix: '/checkout' })async function start() {  await waitForAvsb()  await fastify.listen({ port: 3000 })}start()
TypeScript27 lines
Info

fastifyPlugin is also available from @avsbhq/node, so you can use just one package if you prefer.

Read a flag in a route (src/routes/checkout.ts):

TypeScript
import type { FastifyPluginAsync } from 'fastify'import type { UserBoundClient } from '@avsbhq/node'/** What the plugin decorates onto every request. */interface AvsbRequest {  avsb: UserBoundClient  body: unknown}interface RouteHost {  post(    path: string,    handler: (request: AvsbRequest, reply: unknown) => Promise<unknown>,  ): void}const checkoutRoutes: FastifyPluginAsync = async (fastify: RouteHost) => {  fastify.post('/session', async (request, reply) => {    const checkoutV2 = request.avsb.getBoolFlag('checkout_v2', false)    return {      flow: checkoutV2.value ? 'v2' : 'legacy',      variationKey: checkoutV2.variationKey,    }  })}export default checkoutRoutes
TypeScript27 lines

Track an event:

TypeScript
fastify.post('/purchase', async (request: AvsbRequest, reply: unknown) => {  const body = request.body as { amount: number }  // `revenue` is money in major units; `value` is the separate  // numeric-metric column.  request.avsb.track('purchase', { revenue: body.amount })  return { success: true }})
TypeScript9 lines

Call getRequestClient deep in your own code, for example in a service file (src/services/billingService.ts):

TypeScript
import { getRequestClient } from '@avsbhq/utils'import { applyBulkDiscount, applyStandardPricing } from './pricing'export async function applyDiscount(userId: string) {  // Returns null outside a request scope, so handle that branch.  const client = getRequestClient()  if (!client) return applyStandardPricing(userId)  const discountFlag = client.getStringFlag('discount_strategy', 'none')  if (discountFlag.value === 'bulk') {    return applyBulkDiscount(userId)  }  return applyStandardPricing(userId)}
TypeScript16 lines

TypeScript type augmentation

Declare request.avsb once, in your own project (src/types/fastify.d.ts). You do not add any import for this. Your project owns the declaration, so it never clashes with another package's copy.

TypeScript
// docs-example: not typechecked here, because it augments the `fastify` module,// and this repo does not install fastify, so the module being augmented does not// exist for the standalone TypeScript program this documentation gate builds.import type { UserBoundClient } from '@avsbhq/node'declare module 'fastify' {  interface FastifyRequest {    avsb: UserBoundClient  }}
TypeScript10 lines

Graceful shutdown

Fastify has a built-in onClose hook. Use it to send any pending events before the server shuts down:

TypeScript
fastify.addHook('onClose', async () => {  await avsb.close()})process.on('SIGTERM', async () => {  await fastify.close()})
TypeScript7 lines

Testing

Use @avsbhq/test to fake the server in your route tests, so a test never calls the real API.

TypeScript
import { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test'import { build } from './server' // factory that returns the fastify instanceconst td = TestData.flag('checkout_v2').booleanFlag().fallthroughVariation(true)const mockClient = createMockServer(flagsFromTestData([td.build()]))// Override the plugin's server with a mock before running routes.const app = build({ avsbServer: mockClient })const res = await app.inject({ method: 'POST', url: '/checkout/session' })expect(JSON.parse(res.body).flow).toBe('v2')
TypeScript11 lines

What's next

Was this helpful?