Express integration
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.
Install
1npm install @avsbhq/node@^1 @avsbhq/utils@^1Obtain your SDK key
Open Settings > Environments in your A vs B project and copy the server-side SDK key. Add it to your environment configuration:
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxxInitialise 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.
1import { AvsbServer } from '@avsbhq/node'2
3export const avsb = new AvsbServer({4 sdkKey: process.env.AVSB_SDK_KEY!,5 // Optional: stream real-time datafile updates.6 // streaming: true,7})8
9export async function waitForAvsb(): Promise<void> {10 const result = await avsb.onReady()11 if (!result.success && !result.degraded) {12 console.warn('[avsb] SDK init failed — serving defaults', result.error)13 }14}Mount the middleware
The expressMiddleware from @avsbhq/utils/middleware/express opens an AsyncLocalStorage scope per request. It decorates req.avsb with a UserBoundClientscoped to the current user's context.
1import express from 'express'2import { expressMiddleware } from '@avsbhq/utils/middleware/express'3import { avsb, waitForAvsb } from './avsb'4
5const app = express()6
7app.use(express.json())8
9// Mount the A vs B middleware early so all routes can access req.avsb.10app.use(11 expressMiddleware(avsb, {12 contextFrom: (req) => {13 // Build the EvalContext from request data.14 // Return undefined to skip flag evaluation for this request.15 const uid = req.headers['x-user-id'] as string | undefined16 if (!uid) return undefined17 return { kind: 'user', key: uid }18 },19 withDecisionLog: true,20 })21)22
23async function start() {24 await waitForAvsb()25 app.listen(3000, () => console.log('Server running on :3000'))26}27
28start()expressMiddleware and the middleware options shape come from @avsbhq/utils/middleware/express, which maps to spec §6.9. The exact import path may shift before V1 ships.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.
1import { Router } from 'express'2
3const router = Router()4
5router.post('/checkout', (req, res) => {6 const checkoutV2 = req.avsb.getBoolFlag('checkout-v2', false)7
8 if (checkoutV2.value) {9 return res.json({ flow: 'v2', variationKey: checkoutV2.variationKey })10 }11
12 return res.json({ flow: 'legacy' })13})14
15export default routerTrack an event
1router.post('/purchase', (req, res) => {2 const { amount } = req.body as { amount: number }3
4 req.avsb.track('purchase', {5 value: amount,6 properties: { source: 'checkout_api' },7 })8
9 res.json({ success: true })10})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 getRequestClientfrom @avsbhq/utils/middleware/express. It reads the current context from the AsyncLocalStorage scope:
1import { getRequestClient } from '@avsbhq/utils'2
3export async function processPayment(amount: number) {4 // No req argument needed — context flows via AsyncLocalStorage.5 const client = getRequestClient()6 const splitPayFlag = client.getBoolFlag('split-payment', false)7
8 if (splitPayFlag.value) {9 return runSplitPayment(amount)10 }11
12 return runStandardPayment(amount)13}Graceful shutdown
Call avsb.close() in your SIGTERM handler to flush any pending events before the process exits:
1process.on('SIGTERM', async () => {2 const httpServer = app.listen(...) // keep a reference at startup3 httpServer.close(async () => {4 await avsb.close()5 process.exit(0)6 })7})TypeScript type augmentation
Import the Express type augmentation from @avsbhq/node/express to get full type coverage on req.avsb:
1import '@avsbhq/node/express'2// req.avsb is now typed as UserBoundClientTesting
1import { createMockClient, TestData } from '@avsbhq/test'2import request from 'supertest'3import { app } from './server'4
5const td = TestData.flag('checkout-v2').booleanFlag().fallthroughVariation(true)6const mockClient = createMockClient({ flags: [td.build()] })7
8// Inject mock into the test server by overriding the middleware.9app.request.avsb = mockClient.forUser({ kind: 'user', key: 'test-user' })10
11const res = await request(app).post('/checkout').send({})12expect(res.body.flow).toBe('v2')What's next
- Multi-context identity
- Decision logging — stream per-request decisions to a warehouse sink.
@avsbhq/nodeAPI reference