/Docs

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.

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

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 NestJS config:

.env
bash
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
3

Create 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.

src/avsb/avsb.module.ts
ts
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 server
19 },
20 inject: [ConfigService],
21 },
22 ],
23 exports: [AVSB_SERVER],
24})
25export class AvsbModule {}
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.

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

Register the module

src/app.module.ts
ts
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 {}
6

Read a flag in a controller

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

src/checkout/checkout.controller.ts
ts
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}
7

Track an event

ts
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}
8

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.

src/main.ts
ts
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 | undefined
16 if (!uid) return undefined
17 return { kind: 'user', key: uid }
18 },
19 })
20 )
21
22 await app.listen(3000)
23}
24
25bootstrap()
src/pricing/pricing.service.ts
ts
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 : base
10 }
11}

Graceful shutdown

Enable NestJS shutdown hooks and add a lifecycle hook to the module:

src/main.ts
ts
1const app = await NestFactory.create(AppModule)
2app.enableShutdownHooks()
3await app.listen(3000)
src/avsb/avsb.module.ts (with shutdown)
ts
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

ts
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 unknown
20expect(controller.createSession(req).flow).toBe('v2')

What's next