/Docs

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.

1

Install

bash
1npm install @avsbhq/node@^1 @avsbhq/utils@^1
2

Obtain 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:

.env
bash
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
Info
Server SDK keys are never exposed to the browser. Keep them in environment variables and out of source control.
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.

src/avsb.ts
ts
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}
4

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.

src/server.ts
ts
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 | undefined
16 if (!uid) return undefined
17 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()
Warning
The exported names 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.
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.

src/routes/checkout.ts
ts
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 router
6

Track an event

ts
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})
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 getRequestClientfrom @avsbhq/utils/middleware/express. It reads the current context from the AsyncLocalStorage scope:

src/services/paymentService.ts
ts
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:

ts
1process.on('SIGTERM', async () => {
2 const httpServer = app.listen(...) // keep a reference at startup
3 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:

src/types/express.d.ts
ts
1import '@avsbhq/node/express'
2// req.avsb is now typed as UserBoundClient

Testing

ts
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