NestJS
This guide assumes a NestJS 10+ application running on Node.js 18+. By the end you'll have AvsbService injectable into any controller or service, with per-request context scoping via a request-scoped provider, using @avsbhq/node.
A first-party AvsbModule NestJS package is planned. In the meantime this guide shows a thin module shim you own in your codebase. The shim is straightforward and tracks the stable public surface of @avsbhq/node.
Install
Install @avsbhq/node and @avsbhq/utils.
Obtain your SDK key
Open Environments in your A vs B project sidebar and copy the SDK key. Add it to your NestJS config.
Create the AvsbModule shim
Create an AvsbModule that wraps AvsbServer as a global, singleton provider. Export AVSB_SERVER so it can be injected by token throughout the application.
Create the AvsbService
Create a thin service wrapper that controllers and services inject. It exposes forUser so callers can create a request-scoped bound client.
Register the module
Import AvsbModule into your AppModule alongside your other modules.
Read a flag in a controller
Inject AvsbService and call forUser with the context built from the incoming request.
Track an event
Call track on the injected AvsbService, passing the context for the current user.
Use AsyncLocalStorage for deep service access
For services nested several layers deep, mount the expressMiddleware globally. NestJS defaults to an Express adapter, so this works out of the box. Then use getRequestClient instead of threading context through every call.
Install the packages:
npm install @avsbhq/node@^1 @avsbhq/utils@^1Add the SDK key to your NestJS config (.env):
AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxxCreate the AvsbModule shim (src/avsb/avsb.module.ts):
import { Module, Global, OnApplicationBootstrap } from '@nestjs/common'import { AvsbServer } from '@avsbhq/node'import { ConfigService } from '@nestjs/config'export const AVSB_SERVER = Symbol('AVSB_SERVER')@Global()@Module({ providers: [ { provide: AVSB_SERVER, useFactory: async (config: ConfigService) => { const server = new AvsbServer({ sdkKey: config.getOrThrow('AVSB_SDK_KEY') }) const result = await server.onReady() if (!result.success && !result.degraded) { console.warn('[avsb] degraded init', result.error) } return server }, inject: [ConfigService], }, ], exports: [AVSB_SERVER],})export class AvsbModule {}Create the AvsbService: a thin wrapper that exposes forUser for a request-scoped bound client (src/avsb/avsb.service.ts):
// docs-example: not typechecked here, because NestJS parameter decorators such as// `@Inject()` require experimentalDecorators, which the standalone TypeScript// program this documentation gate builds does not enable.import { Injectable, Inject } from '@nestjs/common'import { AvsbServer } from '@avsbhq/node'import type { EvalContext, TrackPayload, UserBoundClient } from '@avsbhq/node'import { AVSB_SERVER } from './avsb.module'@Injectable()export class AvsbService { constructor(@Inject(AVSB_SERVER) private readonly server: AvsbServer) {} forUser(context: EvalContext): UserBoundClient { return this.server.forUser(context) } track(eventKey: string, payload: TrackPayload & { context: EvalContext }): void { this.server.track(eventKey, payload) } async close(): Promise<void> { await this.server.close() }}Register the module (src/app.module.ts):
import { Module } from '@nestjs/common'import { ConfigModule } from '@nestjs/config'import { AvsbModule } from './avsb/avsb.module'import { CheckoutModule } from './checkout/checkout.module'@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), AvsbModule, CheckoutModule, ],})export class AppModule {}Read a flag in a controller: inject AvsbService and call forUser with the request context (src/checkout/checkout.controller.ts):
// docs-example: not typechecked here, because NestJS parameter decorators such as// `@Req()` require experimentalDecorators, which the standalone TypeScript program// this documentation gate builds does not enable.import { Controller, Post, Req } from '@nestjs/common'import { Request } from 'express'import { AvsbService } from '../avsb/avsb.service'@Controller('checkout')export class CheckoutController { constructor(private readonly avsb: AvsbService) {} @Post('session') createSession(@Req() req: Request) { const uid = req.headers['x-user-id'] as string ?? 'anonymous' const client = this.avsb.forUser({ kind: 'user', key: uid }) const checkoutV2 = client.getBoolFlag('checkout_v2', false) return { flow: checkoutV2.value ? 'v2' : 'legacy', variationKey: checkoutV2.variationKey ?? null, } }}Track an event:
// docs-example: not typechecked here, because NestJS parameter decorators such as// `@Req()` and `@Body()` require experimentalDecorators, which the standalone// TypeScript program this documentation gate builds does not enable.@Controller('checkout')export class CheckoutController { constructor(private readonly avsb: AvsbService) {} @Post('purchase') completePurchase(@Req() req: Request, @Body() body: { amount: number }) { const uid = req.headers['x-user-id'] as string ?? 'anonymous' // `revenue` is money in major units; `value` is the separate // numeric-metric column. this.avsb.track('purchase', { context: { kind: 'user', key: uid }, revenue: body.amount, }) return { success: true } }}Use AsyncLocalStorage for deep service access: mount the expressMiddleware globally (NestJS defaults to an Express adapter) so deep services can use getRequestClient (src/main.ts):
import { NestFactory } from '@nestjs/core'import { expressMiddleware } from '@avsbhq/utils/middleware/express'import { AppModule } from './app.module'import { AVSB_SERVER } from './avsb/avsb.module'import type { AvsbServer } from '@avsbhq/node'async function bootstrap() { const app = await NestFactory.create(AppModule) const avsbServer = app.get<AvsbServer>(AVSB_SERVER) app.use( expressMiddleware(avsbServer, { contextFrom: (req) => { // `contextFrom` receives the raw request, so narrow what you read. const headers = req.headers as Record<string, string | undefined> const uid = headers['x-user-id'] if (!uid) return undefined return { kind: 'user', key: uid } }, }) ) await app.listen(3000)}bootstrap()The deep service then reads the current client via getRequestClient (src/pricing/pricing.service.ts):
import { Injectable } from '@nestjs/common'import { getRequestClient } from '@avsbhq/utils'@Injectable()export class PricingService { getDynamicPrice(base: number): number { // Returns null outside a request scope, so handle that branch. const client = getRequestClient() if (!client) return base const flag = client.getStringFlag('pricing_strategy', 'standard') return flag.value === 'premium' ? base * 1.15 : base }}Graceful shutdown
Enable NestJS shutdown hooks and add a lifecycle hook to the module (src/main.ts):
const app = await NestFactory.create(AppModule)app.enableShutdownHooks()await app.listen(3000)Then add a shutdown service (src/avsb/avsb.module.ts with shutdown):
// docs-example: not typechecked here, because the `@Inject()` parameter decorator// requires experimentalDecorators, which the standalone TypeScript program this// documentation gate builds does not enable.import { OnApplicationShutdown, Inject } from '@nestjs/common'import { AvsbServer } from '@avsbhq/node'import { AVSB_SERVER } from './avsb.module'// Add to AvsbModule providers array:@Injectable()export class AvsbShutdownService implements OnApplicationShutdown { constructor(@Inject(AVSB_SERVER) private readonly server: AvsbServer) {} async onApplicationShutdown(): Promise<void> { await this.server.close() }}Testing
import { Test, TestingModule } from '@nestjs/testing'import { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test'import { CheckoutController } from './checkout.controller'import { AvsbService } from '../avsb/avsb.service'import { AVSB_SERVER } from '../avsb/avsb.module'const td = TestData.flag('checkout_v2').booleanFlag().fallthroughVariation(true)const mockServer = createMockServer(flagsFromTestData([td.build()]))const module: TestingModule = await Test.createTestingModule({ controllers: [CheckoutController], providers: [ AvsbService, { provide: AVSB_SERVER, useValue: mockServer }, ],}).compile()const controller = module.get(CheckoutController)const req = { headers: { 'x-user-id': 'u_test' } } as unknownexpect(controller.createSession(req).flow).toBe('v2')