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.

Info

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.

1

Install

Install @avsbhq/node and @avsbhq/utils.

2

Obtain your SDK key

Open Environments in your A vs B project sidebar and copy the SDK key. Add it to your NestJS config.

3

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.

4

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.

5

Register the module

Import AvsbModule into your AppModule alongside your other modules.

6

Read a flag in a controller

Inject AvsbService and call forUser with the context built from the incoming request.

7

Track an event

Call track on the injected AvsbService, passing the context for the current user.

8

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:

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

Add the SDK key to your NestJS config (.env):

Shell
AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
Shell1 line

Create the AvsbModule shim (src/avsb/avsb.module.ts):

TypeScript
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 {}
TypeScript25 lines

Create the AvsbService: a thin wrapper that exposes forUser for a request-scoped bound client (src/avsb/avsb.service.ts):

TypeScript
// 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()  }}
TypeScript24 lines

Register the module (src/app.module.ts):

TypeScript
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 {}
TypeScript13 lines

Read a flag in a controller: inject AvsbService and call forUser with the request context (src/checkout/checkout.controller.ts):

TypeScript
// 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,    }  }}
TypeScript24 lines

Track an event:

TypeScript
// 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 }  }}
TypeScript21 lines

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

TypeScript
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()
TypeScript27 lines

The deep service then reads the current client via getRequestClient (src/pricing/pricing.service.ts):

TypeScript
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  }}
TypeScript14 lines

Graceful shutdown

Enable NestJS shutdown hooks and add a lifecycle hook to the module (src/main.ts):

TypeScript
const app = await NestFactory.create(AppModule)app.enableShutdownHooks()await app.listen(3000)
TypeScript3 lines

Then add a shutdown service (src/avsb/avsb.module.ts with shutdown):

TypeScript
// 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()  }}
TypeScript16 lines

Testing

TypeScript
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')
TypeScript20 lines

What's next

Was this helpful?