Express

This guide assumes an Express 4 or 5 server running on Node.js 18+. By the end you'll have per-request flag evaluation scoped to each user's context using @avsbhq/node and the @avsbhq/utils/middleware/express adapter.

1

Install

Install @avsbhq/node and @avsbhq/utils.

2

Obtain your SDK key

Open your A vs B project and select Environments in the sidebar, then copy the SDK key. Add it to your environment configuration.

3

Initialise the server SDK

Create a singleton AvsbServer instance at application startup. Call onReady() before the server starts accepting traffic so the first request always has the latest datafile.

4

Mount the middleware

The expressMiddleware from @avsbhq/utils/middleware/express opens an AsyncLocalStorage scope per request. It decorates req.avsb with a UserBoundClient scoped to the current user's context.

5

Read a flag in a route handler

Access req.avsb to read flags for the current user. The client is already bound to the request context, no need to pass the user identity again.

6

Track an event

Call req.avsb.track to record conversion events for the current user.

7

Use AsyncLocalStorage for implicit context

For utilities or services that are called from inside a request handler but do not have access to req, use getRequestClient from @avsbhq/utils/middleware/express. It reads the current context from the AsyncLocalStorage scope.

Install the packages:

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

Add the SDK key to your environment configuration (.env):

Shell
AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
Shell1 line
Your SDK key is public
Your SDK key is a public identifier, not a secret: it is safe to ship in browser and mobile bundles, it can only fetch that environment's flag configuration and send events, and it can never read or change anything in your dashboard. Credentials covers all four A vs B credentials and which one to reach for.

Keep the key in an environment variable rather than in source, the same as any other configuration: hardcoding it makes changing environments a code change.

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.

Initialise the server SDK (src/avsb.ts):

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

Mount the middleware early so all routes can access req.avsb (src/server.ts):

TypeScript
import express from 'express'import { expressMiddleware } from '@avsbhq/utils/middleware/express'import { avsb, waitForAvsb } from './avsb'const app = express()app.use(express.json())// Mount the A vs B middleware early so all routes can access req.avsb.app.use(  expressMiddleware(avsb, {    contextFrom: (req) => {      // Build the EvalContext from request data.      // Return undefined to skip flag evaluation for this request.      // `contextFrom` receives the raw request, so narrow what you read off it.      const headers = req.headers as Record<string, string | undefined>      const uid = headers['x-user-id']      if (!uid) return undefined      return { kind: 'user', key: uid }    },    withDecisionLog: true,  }))async function start() {  await waitForAvsb()  app.listen(3000, () => console.log('Server running on :3000'))}start()
TypeScript30 lines
Info

expressMiddleware lives in @avsbhq/utils/middleware/express and is re-exported from @avsbhq/node, so either import works and both are the same function. There is one signature: expressMiddleware(server, { contextFrom, withDecisionLog? }). Return undefined from contextFrom to skip AvsB for a request.

The one mistake: req.avsb is not always there

When contextFrom returns undefined, the middleware calls next() immediately and never sets req.avsb at all. It is not a client with no context: the property is simply missing. A route handler that skips the check and calls req.avsb.getBoolFlag(...) throws Cannot read properties of undefined. The type augmentation below declares avsb as always present, so TypeScript won't catch this either. Guard for it: if (!req.avsb) return next(), or default the flow when contextFrom has nothing to key on (no session cookie, a health check route, and so on).

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

TypeScript
import { Router } from 'express'import type { RequestBoundClient } from '@avsbhq/utils'const router = Router()/** What the middleware adds to every request it handles. */interface AvsbRequest {  avsb: RequestBoundClient  body: unknown}interface JsonResponse {  json(body: unknown): unknown}router.post('/checkout', (req: AvsbRequest, res: JsonResponse) => {  const checkoutV2 = req.avsb.getBoolFlag('checkout_v2', false)  if (checkoutV2.value) {    return res.json({ flow: 'v2', variationKey: checkoutV2.variationKey })  }  return res.json({ flow: 'legacy' })})export default router
TypeScript25 lines

Track an event:

TypeScript
router.post('/purchase', (req: AvsbRequest, res: JsonResponse) => {  const { amount } = req.body as { amount: number }  // `revenue` is money in major units; `value` is the separate numeric-metric  // column (items in a cart, seats on a plan).  req.avsb.track('purchase', { revenue: amount })  res.json({ success: true })})
TypeScript9 lines

Use AsyncLocalStorage for implicit context: service functions without access to req can read the current context via getRequestClient (src/services/paymentService.ts):

TypeScript
import { getRequestClient } from '@avsbhq/utils'import { runSplitPayment, runStandardPayment } from './payments'export async function processPayment(amount: number) {  // No req argument needed: context flows via AsyncLocalStorage.  // It returns null outside a request scope, so handle that branch.  const client = getRequestClient()  if (!client) return runStandardPayment(amount)  const splitPayFlag = client.getBoolFlag('split_payment', false)  if (splitPayFlag.value) {    return runSplitPayment(amount)  }  return runStandardPayment(amount)}
TypeScript17 lines

Graceful shutdown

Call avsb.close() in your SIGTERM handler to flush any pending events before the process exits:

TypeScript
// Keep the reference `app.listen()` returns at startup.const httpServer = app.listen(3000)process.on('SIGTERM', () => {  httpServer.close(async () => {    await avsb.close()    process.exit(0)  })})
TypeScript9 lines

TypeScript type augmentation

Declare req.avsb once in your own project (src/types/express.d.ts). There is no import to add: your project owns the augmentation, so it never fights another package's copy.

TypeScript
import type { UserBoundClient } from '@avsbhq/node'declare global {  namespace Express {    interface Request {      avsb: UserBoundClient    }  }}
TypeScript9 lines

Testing

TypeScript
import { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test'import request from 'supertest'import { app } from './server'const td = TestData.flag('checkout_v2').booleanFlag().fallthroughVariation(true)const mockServer = createMockServer(flagsFromTestData([td.build()]))// Inject the mock into the test server by overriding the middleware.app.request.avsb = mockServer.forUser({ kind: 'user', key: 'test-user' })const res = await request(app).post('/checkout').send({})expect(res.body.flow).toBe('v2')
TypeScript12 lines

The mock has the whole AvsbServer surface, so code that calls overrides, alias, refreshDatafile, server.utils, server.datasets or server.recs runs under it unchanged. Datasets and recommendations answer from two more options:

TypeScript
const mockServer = createMockServer({  flags: { checkout_v2: true },  datasets: { prices: { sku_1: { amount: 1999 } } }, // dataset slug, then key, then value  recs: { similar: [{ id: 'p1' }, { id: 'p2' }] }, // recipe, then the products it returns})
TypeScript5 lines

What's next

Was this helpful?