Connect a NestJS app

Add ApiTraffic to a NestJS API: install the module, set credentials, see what is recorded, and turn on the firewall, APM, tagging and tracing.

This page as Markdown

@apitraffic/nestjs is a Nest module. Import it once, in your root module, and every request your application serves is recorded and checked by the ApiTraffic firewall - on the Express platform (Nest’s default) and on the Fastify platform alike, with nothing to change in main.ts.

It is @apitraffic/node, the one-install SDK, wired into Nest: the same capture, the same firewall and APM bridge, the same helpers, all enabled from your ApiTraffic account rather than in code.

Installation

npm

bash
npm install @apitraffic/nestjs

yarn

bash
yarn add @apitraffic/nestjs

pnpm

bash
pnpm add @apitraffic/nestjs

Note: Node 20+ and NestJS 10, 11 or 12 required. NestJS 12 is an ES module, which this package loads with require(): that needs Node 20.19+ or 22.12+.

Basic Setup

typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ApiTrafficModule } from '@apitraffic/nestjs';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    ApiTrafficModule.forRoot({
      token: process.env.API_TRAFFIC_TOKEN,
      bucket: process.env.API_TRAFFIC_BUCKET,
    }),
    UsersModule,
  ],
})
export class AppModule {}

The module is global: import it once, in the root module, and the helpers below work in every module of the application.

Options from configuration

forRootAsync() takes the options from a factory, with injected providers, as other Nest modules do:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ApiTrafficModule } from '@apitraffic/nestjs';

@Module({
  imports: [
    ApiTrafficModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        token: config.get('API_TRAFFIC_TOKEN'),
        bucket: config.get('API_TRAFFIC_BUCKET'),
      }),
    }),
  ],
})
export class AppModule {}

What is recorded

Every request, including the ones a controller never sees. The module runs ahead of Nest’s body parser, its middleware, guards and router, so these are all recorded, with their final status and whatever your code tagged on the way:

  • a request a guard rejects (401, 403),
  • a request a middleware answers itself (an auth middleware’s 401),
  • a request no route matches (404),
  • a request whose handler throws, as the response Nest’s exception filters sent.

The route Nest matched is sent with each request, global prefix and URI version included, so @Get(':id') in @Controller('users') under setGlobalPrefix('api') and version 1 is the endpoint /api/v1/users/{id}. See Endpoint Handling.

On the Express platform requests are captured by @apitraffic/express, on the Fastify platform by @apitraffic/fastify: exactly what the Express and Fastify pages describe under “What is captured” holds here too.

Tagging, tracing and spans

The helpers work anywhere a request is being handled: controllers, services, guards, interceptors, pipes, exception filters and middleware.

typescript
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { tag } from '@apitraffic/nestjs';

@Injectable()
export class AccountGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // Searchable on this request, whether or not the guard lets it through.
    tag('accountId', request.headers['x-account-id']);
    return true;
  }
}
typescript
import { Injectable } from '@nestjs/common';
import { span, trace } from '@apitraffic/nestjs';

@Injectable()
export class UsersService {
  async findOne(id: string) {
    trace(`Loading user ${id}`);
    return span('db.query', 'load user', () => this.repository.findOneBy({ id }));
  }
}

Outside a request (a scheduled job, a queue worker) the helpers do nothing and never throw.

stats() reports the SDK’s delivery health (delivered, queued, dropped), for a health endpoint or a log line.

Shutdown

app.close(), and the signals app.enableShutdownHooks() listens for, deliver what was captured and release the SDK’s timers and connections once the last application using the module has closed. The HTTP server has closed by then, so every request it served has been reported.

typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  await app.listen(3000);
}

bootstrap();

Firewall

The ApiTraffic firewall is mounted by the module, and whether it runs is decided in your ApiTraffic account: nothing is loaded until you switch a policy on, and a policy change reaches every server without a deploy. It runs where Nest has parsed the body - after Nest’s body parser on the Express platform, in Fastify’s preHandler on the Fastify platform - so body rules see the parsed body, and ahead of your guards and controllers, so a request it blocks never reaches your code. Blocked requests are recorded with the policy’s status and tagged waf.*.

Middleware you apply with MiddlewareConsumer runs after the firewall on the Express platform. On the Fastify platform Nest runs middleware in Fastify’s onRequest stage, before the body is parsed, so there it runs ahead of the firewall, as in any Fastify application that uses middleware.

To refuse it locally whatever the dashboard says (change control, or an incident switch that does not depend on the control plane), pass waf: false or set API_TRAFFIC_WAF_ENABLED=false:

typescript
ApiTrafficModule.forRoot({ waf: false });

See Firewall setup for policies and rules.

Application Performance Monitoring (APM)

The APM bridge captures database, cache, queue, GraphQL, gRPC, and fetch/undici spans within each ApiTraffic request. Install the OpenTelemetry peers it uses:

bash
npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @opentelemetry/auto-instrumentations-node

OpenTelemetry patches modules as they are loaded, so it has to start before Nest and your database clients do. Preload it:

bash
node -r @apitraffic/nestjs/register dist/main.js

or call register() on the very first line of main.ts, ahead of every other import:

typescript
import { register } from '@apitraffic/nestjs';
register();

If your application already configures OpenTelemetry, add ApiTrafficSpanProcessor from @apitraffic/apm to that provider instead, so spans are not double-instrumented.

Configuration Options

Option Environment Variable Required Type Description
token API_TRAFFIC_TOKEN Yes String Ingest token from your ApiTraffic account
bucket API_TRAFFIC_BUCKET Yes String Bucket ID for data organization
interceptOutbound API_TRAFFIC_INTERCEPT_OUTBOUND No Boolean Monitor outbound HTTP requests (default: true)
debug API_TRAFFIC_DEBUG No Boolean Enable debug logging (default: false)
waf API_TRAFFIC_WAF_ENABLED No Boolean or object false refuses the firewall locally; an object passes settings to @apitraffic/waf. Normally left unset: the firewall is switched on from your account

Every option falls back to its environment variable, so ApiTrafficModule.forRoot() with no options at all reads its configuration from the environment. An option passed in code wins over the environment.

The options are typed: ApiTrafficOptions lists every option the SDK accepts, a value of the wrong type is a compile error, and so is a misspelled key in an object literal.

typescript
import { ApiTrafficModule, type ApiTrafficOptions } from '@apitraffic/nestjs';

const options: ApiTrafficOptions = { debug: process.env.NODE_ENV !== 'production' };
ApiTrafficModule.forRoot(options);

Embed tokens

The embeddable request log authenticates with a token your server mints. embedToken is exported here, as from every ApiTraffic package:

typescript
import { Controller, Get } from '@nestjs/common';
import { embedToken } from '@apitraffic/nestjs';

@Controller('embed')
export class EmbedController {
  @Get('token')
  token() {
    return embedToken({ filters: { tags: { customerId: 'cus_123' } }, ttl: 900 });
  }
}

Troubleshooting

No requests are recorded

Check that ApiTrafficModule is imported in the module you pass to NestFactory.create() (or to Test.createTestingModule()), and that API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET are set. Set debug: true to see what the SDK is doing.

Tags set in a scheduled job or queue consumer are missing

Tags, traces and spans belong to an HTTP request. Code that does not run while a request is being handled has none to attach them to, and the helpers do nothing there.

Two applications in one process

Each application records its own requests. The SDK is shut down when the last of them closes.

Sample Application

A complete working example is available in the GitHub repository.