NestJS
NestJS wants the client as a provider — injectable, mockable, and shut down through the
framework's lifecycle hooks rather than a raw process listener.
1. Install
npm i bugboard
BUGBOARD_KEY_ID=bbk_xxxxxxxx BUGBOARD_SIGNING_SECRET=bb_sec_xxxxxxxx
2. Register the client as a provider
// src/bugboard/bugboard.module.ts import { Global, Module } from '@nestjs/common'; import { createClient, type BugBoardClient } from 'bugboard'; export const BUGBOARD = Symbol('BUGBOARD'); @Global() @Module({ providers: [ { provide: BUGBOARD, useFactory: (): BugBoardClient => createClient({ keyId: process.env.BUGBOARD_KEY_ID, signingSecret: process.env.BUGBOARD_SIGNING_SECRET, environment: process.env.NODE_ENV, }), }, ], exports: [BUGBOARD], }) export class BugBoardModule {}
@Global() means you import the module once in AppModule and inject BUGBOARD anywhere.
Because the provider is a singleton, there is exactly one queue and one shutdown hook.
3. Report from an exception filter
The natural reporting point:
// src/bugboard/bugboard-exception.filter.ts import { ArgumentsHost, Catch, HttpException, Inject } from '@nestjs/common'; import { BaseExceptionFilter } from '@nestjs/core'; import type { BugBoardClient } from 'bugboard'; import { BUGBOARD } from './bugboard.module'; @Catch() export class BugBoardExceptionFilter extends BaseExceptionFilter { constructor(@Inject(BUGBOARD) private readonly bugboard: BugBoardClient) { super(); } catch(exception: unknown, host: ArgumentsHost) { const status = exception instanceof HttpException ? exception.getStatus() : 500; if (status >= 500) { const req = host.switchToHttp().getRequest(); this.bugboard.criticalHigh( `Unhandled error: ${req.method} ${req.route?.path}`, exception, ['nestjs'], ); } super.catch(exception, host); } }
Two things to keep: the status >= 500 guard (4xx are client mistakes, not your bugs) and
req.route?.path — the route pattern, so repeated failures deduplicate into one card
instead of one per id.
Injecting anywhere else works the same way:
@Injectable() export class CheckoutService { constructor(@Inject(BUGBOARD) private readonly bugboard: BugBoardClient) {} async charge(order: Order) { try { await this.payments.charge(order); } catch (err) { this.bugboard.criticalHigh('Payment capture failed', err, [ 'payments', ]); throw err; } } }
4. Flush on shutdown
import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class BugBoardShutdown implements OnApplicationShutdown { constructor(@Inject(BUGBOARD) private readonly bugboard: BugBoardClient) {} async onApplicationShutdown(): Promise<void> { await this.bugboard.flush(); } }
This requires app.enableShutdownHooks() in main.ts. It is what delivers reports across a
SIGTERM — which a containerized service receives on every deploy, and where the SDK's
automatic beforeExit hook does not fire.
If you deploy NestJS to a serverless target instead of a long-running container, flushing is mandatory on every invocation — see Serverless & edge.
Testing
Because the client is injected behind a token, tests override it with a spy — no module mocking:
const module = await Test.createTestingModule({ providers: [CheckoutService, { provide: BUGBOARD, useValue: fakeClient() }], }).compile(); function fakeClient() { return { critical: vi.fn(), criticalHigh: vi.fn(), major: vi.fn(), flush: vi.fn().mockResolvedValue(undefined), } as unknown as BugBoardClient; }
expect(bugboard.criticalHigh).toHaveBeenCalledWith( 'Payment capture failed', expect.any(Error), ['payments'], );
This is the payoff for injecting the client rather than importing a shared module deep in the call stack.
Next steps
- JavaScript SDK — the base guide and the full method surface.
- Node: Express, Fastify, Koa — process-level handlers and
SIGTERMflushing, which apply to NestJS too. - API Reference — every config option and the HTTP contract.