# 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.

Source: https://www.apitraffic.io/docs/integrations/adonisjs

`@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`](https://www.apitraffic.io/docs/integrations/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](https://www.apitraffic.io/docs/endpoint-handling#framework-routes).

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`](https://www.apitraffic.io/docs/integrations/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](https://www.apitraffic.io/docs/account/ingest-rate-limits).

## 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](https://www.apitraffic.io/docs/waf/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](https://www.apitraffic.io/docs/apm/setup#es-modules)):

```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](https://www.apitraffic.io/docs/integrations/embed) 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](https://www.apitraffic.io/docs/integrations/node#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](https://github.com/apitraffic/platform/tree/main/libs/client-libraries/apitraffic-adonisjs/examples/basic).
