NestJS integration
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.
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
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 NestJS config:
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxxCreate the AvsbModule shim
Create a AvsbModule that wraps AvsbServer as a global, singleton provider. Export AVSB_SERVER so it can be injected by token throughout the application.
1import { Module, Global, OnApplicationBootstrap } from '@nestjs/common'2import { AvsbServer } from '@avsbhq/node'3import { ConfigService } from '@nestjs/config'4
5export const AVSB_SERVER = Symbol('AVSB_SERVER')6
7@Global()8@Module({9 providers: [10 {11 provide: AVSB_SERVER,12 useFactory: async (config: ConfigService) => {13 const server = new AvsbServer({ sdkKey: config.getOrThrow('AVSB_SDK_KEY') })14 const result = await server.onReady()15 if (!result.success && !result.degraded) {16 console.warn('[avsb] degraded init', result.error)17 }18 return server19 },20 inject: [ConfigService],21 },22 ],23 exports: [AVSB_SERVER],24})25export class AvsbModule {}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.
1import { Injectable, Inject } from '@nestjs/common'2import { AvsbServer } from '@avsbhq/node'3import type { EvalContext, UserBoundClient } from '@avsbhq/core'4import { AVSB_SERVER } from './avsb.module'5
6@Injectable()7export class AvsbService {8 constructor(@Inject(AVSB_SERVER) private readonly server: AvsbServer) {}9
10 forUser(context: EvalContext): UserBoundClient {11 return this.server.forUser(context)12 }13
14 track(eventKey: string, payload: { context: EvalContext; value?: number; properties?: Record<string, unknown> }): void {15 this.server.track(eventKey, payload)16 }17
18 async close(): Promise<void> {19 await this.server.close()20 }21}Register the module
1import { Module } from '@nestjs/common'2import { ConfigModule } from '@nestjs/config'3import { AvsbModule } from './avsb/avsb.module'4import { CheckoutModule } from './checkout/checkout.module'5
6@Module({7 imports: [8 ConfigModule.forRoot({ isGlobal: true }),9 AvsbModule,10 CheckoutModule,11 ],12})13export class AppModule {}Read a flag in a controller
Inject AvsbService and call forUser with the context built from the incoming request.
1import { Controller, Post, Req } from '@nestjs/common'2import { Request } from 'express'3import { AvsbService } from '../avsb/avsb.service'4
5@Controller('checkout')6export class CheckoutController {7 constructor(private readonly avsb: AvsbService) {}8
9 @Post('session')10 createSession(@Req() req: Request) {11 const uid = req.headers['x-user-id'] as string ?? 'anonymous'12 const client = this.avsb.forUser({ kind: 'user', key: uid })13
14 const checkoutV2 = client.getBoolFlag('checkout-v2', false)15
16 return {17 flow: checkoutV2.value ? 'v2' : 'legacy',18 variationKey: checkoutV2.variationKey ?? null,19 }20 }21}Track an event
1@Post('purchase')2completePurchase(@Req() req: Request, @Body() body: { amount: number }) {3 const uid = req.headers['x-user-id'] as string ?? 'anonymous'4
5 this.avsb.track('purchase', {6 context: { kind: 'user', key: uid },7 value: body.amount,8 properties: { source: 'checkout_controller' },9 })10
11 return { success: true }12}Use AsyncLocalStorage for deep service access
For services nested several layers deep, mount the avsbExpressMiddleware (since NestJS defaults to an Express adapter) globally and use getRequestClient without threading context through every call.
1import { NestFactory } from '@nestjs/core'2import { expressMiddleware } from '@avsbhq/utils/middleware/express'3import { AppModule } from './app.module'4import { AVSB_SERVER } from './avsb/avsb.module'5import type { AvsbServer } from '@avsbhq/node'6
7async function bootstrap() {8 const app = await NestFactory.create(AppModule)9
10 const avsbServer = app.get<AvsbServer>(AVSB_SERVER)11
12 app.use(13 expressMiddleware(avsbServer, {14 contextFrom: (req) => {15 const uid = req.headers['x-user-id'] as string | undefined16 if (!uid) return undefined17 return { kind: 'user', key: uid }18 },19 })20 )21
22 await app.listen(3000)23}24
25bootstrap()1import { Injectable } from '@nestjs/common'2import { getRequestClient } from '@avsbhq/utils'3
4@Injectable()5export class PricingService {6 getDynamicPrice(base: number): number {7 const client = getRequestClient()8 const flag = client.getStringFlag('pricing-strategy', 'standard')9 return flag.value === 'premium' ? base * 1.15 : base10 }11}Graceful shutdown
Enable NestJS shutdown hooks and add a lifecycle hook to the module:
1const app = await NestFactory.create(AppModule)2app.enableShutdownHooks()3await app.listen(3000)1import { OnApplicationShutdown, Inject } from '@nestjs/common'2import { AvsbServer } from '@avsbhq/node'3import { AVSB_SERVER } from './avsb.module'4
5// Add to AvsbModule providers array:6@Injectable()7export class AvsbShutdownService implements OnApplicationShutdown {8 constructor(@Inject(AVSB_SERVER) private readonly server: AvsbServer) {}9
10 async onApplicationShutdown(): Promise<void> {11 await this.server.close()12 }13}Testing
1import { Test, TestingModule } from '@nestjs/testing'2import { createMockClient, TestData } from '@avsbhq/test'3import { CheckoutController } from './checkout.controller'4import { AvsbService } from '../avsb/avsb.service'5import { AVSB_SERVER } from '../avsb/avsb.module'6
7const td = TestData.flag('checkout-v2').booleanFlag().fallthroughVariation(true)8const mockServer = createMockClient({ flags: [td.build()] })9
10const module: TestingModule = await Test.createTestingModule({11 controllers: [CheckoutController],12 providers: [13 AvsbService,14 { provide: AVSB_SERVER, useValue: mockServer },15 ],16}).compile()17
18const controller = module.get(CheckoutController)19const req = { headers: { 'x-user-id': 'u_test' } } as unknown20expect(controller.createSession(req).flow).toBe('v2')