Connect an AdonisJS app

Add ApiTraffic to an AdonisJS 6 or 7 API: install the provider and middleware, credentials, what is recorded, firewall, APM and troubleshooting.

This page as Markdown

@apitraffic/adonisjs is an AdonisJS package: a provider and two middleware. One command installs and registers them, and every request your application serves is recorded and checked by the ApiTraffic firewall.

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

Installation

bash
node ace add @apitraffic/adonisjs

node ace add installs the package with your package manager and configures it. To install it yourself first, run node ace configure @apitraffic/adonisjs afterwards.

Note: AdonisJS 6 or 7 required, on the Node version it supports (Node 20.6+ for AdonisJS 6, Node 24+ for AdonisJS 7).

Basic Setup

configure makes these changes, and running it again adds nothing twice:

  • config/apitraffic.ts, the package’s options (below).
  • .env: API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET, and their validation in start/env.ts. Set them to your ingest token and bucket ID.
  • adonisrc.ts: the provider, which reads config/apitraffic.ts, starts the SDK with the HTTP server and delivers what was captured when the application stops.
  • start/kernel.ts: the ApiTraffic middleware, first in the server middleware, and the firewall, last in the router middleware, after the body parser.
typescript
// start/kernel.ts
server.use([
  () => import('@apitraffic/adonisjs/apitraffic_middleware'),
  () => import('#middleware/container_bindings_middleware'),
  () => import('#middleware/force_json_response_middleware'),
  () => import('@adonisjs/cors/cors_middleware'),
])

router.use([
  () => import('@adonisjs/core/bodyparser_middleware'),
  () => import('@adonisjs/auth/initialize_auth_middleware'),
  () => import('@apitraffic/adonisjs/firewall_middleware'),
])
typescript
// config/apitraffic.ts
import env from '#start/env'
import { defineConfig } from '@apitraffic/adonisjs'

const apiTrafficConfig = defineConfig({
  token: env.get('API_TRAFFIC_TOKEN'),
  bucket: env.get('API_TRAFFIC_BUCKET'),
})

export default apiTrafficConfig

Note: Keep the ApiTraffic middleware first in server.use(). Everything after it, down to your controllers and exception handler, runs inside the request it records, which is what makes tag(), trace() and span() work there.

What is recorded

Every request, including the ones a controller never sees. Server middleware runs whether or not a route matches, so these are all recorded, with their final status and whatever your code tagged on the way:

  • a request a server middleware answers itself (CORS, an auth check),
  • a request a route middleware refuses (auth, guest, your own),
  • a request no route matches (404),
  • a request whose handler throws, as the response your exception handler sent.

The route AdonisJS matched is sent with each request, group prefixes included, so router.get('/users/:id') in a group with .prefix('/api/v1') is the endpoint /api/v1/users/{id}. A wildcard route (/files/*) sends none, and ApiTraffic detects the endpoint itself. See Endpoint Handling.

The request is recorded as your application sees it: the client’s address from request.ip() (your trustProxy setting honoured), the body the body parser parsed (a JSON or form body as fields, a raw type such as text/plain as text), and the response exactly as it was sent, streamed responses and downloads included. Capture is the same code as in @apitraffic/express, so what that page describes under “What is captured” holds here too.

Tagging, tracing and spans

The helpers work anywhere a request is being handled: controllers, services, middleware, route middleware and your exception handler.

typescript
// app/middleware/account_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { tag } from '@apitraffic/adonisjs'

export default class AccountMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    // Searchable on this request, whether or not it gets past this middleware.
    tag('accountId', ctx.request.header('x-account-id') ?? 'none')
    return next()
  }
}
typescript
// app/services/users_service.ts
import { span, trace } from '@apitraffic/adonisjs'
import User from '#models/user'

export default class UsersService {
  async find(id: string) {
    trace(`Loading user ${id}`)
    return span('db.query', 'load user', () => User.findOrFail(id))
  }
}

Outside a request (an Ace command, 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. Over your plan’s ingest rate limit the SDK records a sample of the extra traffic: rateLimited counts what it left out, and each recorded sample carries its sampleRate; see Ingest rate limits and sampling.

Shutdown

When the application terminates (app.terminate(), which the starter kits’ bin/server.ts calls on SIGTERM, and on SIGINT when the app runs under pm2), AdonisJS closes the HTTP server first and then the provider delivers what was captured and releases the SDK’s timers and connections, so every request the server answered has been reported. There is nothing to add to bin/server.ts unless your process is stopped some other way: a plain SIGINT (Ctrl-C) outside pm2 exits without app.terminate(), and what is still queued is not delivered.

The SDK starts with the HTTP server only: Ace commands and the REPL serve no requests and never contact ApiTraffic. In the test environment it starts with the first request a test sends.

Firewall

The ApiTraffic firewall is mounted by the package, 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 checks each request as it arrives, and a request with a body once more when the body parser has read it, without counting it twice against rate limits:

  • in the server middleware, as the request arrives, so a request no route matches is checked too, before any of your server middleware runs;
  • in the router middleware, after the body parser, so body rules see the parsed body, and ahead of named middleware (auth) and your controller.

A request it blocks never reaches the router middleware after it, your named middleware or your controller (one blocked on its body has been through the router middleware listed ahead of the firewall, the body parser among them). It is recorded with the policy’s status and tagged waf.*. A request blocked as it arrives is answered ahead of your server middleware, CORS included, so a browser sees it without Access-Control-* headers. Without the firewall middleware in router.use() the first check still runs, but body rules have no body to inspect.

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

typescript
// config/apitraffic.ts
const apiTrafficConfig = defineConfig({
  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 AdonisJS and your database clients do. An AdonisJS application is an ES module, so it is preloaded with --import, after OpenTelemetry’s loader hook (see APM setup):

javascript
// otel-hook.mjs
import { register } from 'node:module'

register('@opentelemetry/instrumentation/hook.mjs', import.meta.url)
bash
node --import ./otel-hook.mjs --import @apitraffic/adonisjs/register build/bin/server.js

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 an empty defineConfig({}) reads the configuration from the environment. An option set in config/apitraffic.ts wins over the environment. Keep the provider registered: without it the middleware still records requests (with options from the environment), but nothing delivers what is still queued when the application stops.

The options are typed: defineConfig() takes ApiTrafficOptions, which lists every option the SDK accepts, so a value of the wrong type is a compile error, and so is a misspelled key.

Embed tokens

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

typescript
// start/routes.ts
import router from '@adonisjs/core/services/router'
import { embedToken } from '@apitraffic/adonisjs'

router.get('/embed/token', () => embedToken({ filters: { tags: { customerId: 'cus_123' } }, ttl: 900 }))

embedToken() mints with an Administrator API token set as API_TRAFFIC_EMBED_TOKEN, not the ingest token in API_TRAFFIC_TOKEN; see Embed tokens.

Troubleshooting

No requests are recorded

Check that @apitraffic/adonisjs/apitraffic_middleware is in server.use() in start/kernel.ts (not only in router.use(), which runs only for matched routes), 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 middleware are missing

The ApiTraffic middleware must come first in server.use(): server middleware registered ahead of it runs before the request it records has started.

Body rules do not see the request body

The firewall middleware has to come after @adonisjs/core/bodyparser_middleware in router.use(). node ace configure puts it last.

Several applications in one process (a test suite)

The SDK is set up once per process, with the first application’s config/apitraffic.ts, and shut down when the last application serving HTTP terminates. After that it stays down, as shutdown() leaves it: requests are still served, and not recorded.

Tags set in an Ace command or a 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.

Sample Application

A complete working example is available in the GitHub repository.