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.
@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
npm install @apitraffic/nestjs
yarn
yarn add @apitraffic/nestjs
pnpm
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
// 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:
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.
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;
}
}
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.
// 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:
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:
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:
node -r @apitraffic/nestjs/register dist/main.js
or call register() on the very first line of main.ts, ahead of every other import:
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.
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:
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.