NestJS
Integrate vTilt with NestJS — injectable provider wrapping the Node SDK, request-scoped capture, graceful shutdown.
NestJS uses dependency injection, so the cleanest integration is a global module that exposes a singleton VTiltService. Your controllers and services then inject it like any other provider.
#1. Install
npm install @v-tilt/nodebash
#2. Create a service
// src/vtilt/vtilt.service.ts
import { Injectable, OnModuleDestroy } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { VTiltNode } from '@v-tilt/node'
@Injectable()
export class VTiltService implements OnModuleDestroy {
private client: VTiltNode
constructor(private config: ConfigService) {
this.client = new VTiltNode(this.config.getOrThrow('VTILT_TRACKER_TOKEN'), {
host: this.config.get('VTILT_HOST'),
})
}
capture(input: Parameters<VTiltNode['capture']>[0]) {
return this.client.capture(input)
}
identify(input: Parameters<VTiltNode['identify']>[0]) {
return this.client.identify(input)
}
alias(input: Parameters<VTiltNode['alias']>[0]) {
return this.client.alias(input)
}
async onModuleDestroy() {
await this.client.shutdown()
}
}typescript
#3. Register a global module
// src/vtilt/vtilt.module.ts
import { Global, Module } from '@nestjs/common'
import { VTiltService } from './vtilt.service'
@Global()
@Module({
providers: [VTiltService],
exports: [VTiltService],
})
export class VTiltModule {}typescript
// src/app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { VTiltModule } from './vtilt/vtilt.module'
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), VTiltModule],
})
export class AppModule {}typescript
# .env
VTILT_TRACKER_TOKEN=YOUR_PROJECT_TOKEN
VTILT_HOST=https://www.vtilt.comtext
#4. Capture from a controller
// src/checkout/checkout.controller.ts
import { Body, Controller, Post } from '@nestjs/common'
import { VTiltService } from '../vtilt/vtilt.service'
@Controller('checkout')
export class CheckoutController {
constructor(private vt: VTiltService) {}
@Post()
complete(@Body() body: { userId: string; amount: number }) {
this.vt.capture({
distinctId: body.userId,
event: 'purchase_completed',
properties: { amount: body.amount },
})
return { ok: true }
}
}typescript
#5. Identify users from a guard or interceptor
If you have an AuthGuard that resolves the current user, identify them once per request so any browser-side anonymous activity merges to the right person record.
// src/vtilt/vtilt.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { VTiltService } from './vtilt.service'
@Injectable()
export class VTiltIdentifyInterceptor implements NestInterceptor {
constructor(private vt: VTiltService) {}
intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = ctx.switchToHttp().getRequest()
const user = req.user
const anonymousId = req.cookies?.vt_anon
if (user) {
this.vt.identify({
distinctId: user.id,
anonymousId,
properties: { email: user.email },
})
}
return next.handle()
}
}typescript
#Next steps
- Node SDK / Capture, identify & alias — server-side event shapes.
- Identify & alias (browser) — anonymous → known user merge model.
- Event forwarding — fan events out to GA4, Meta CAPI, PostHog.