Connect a hapi app

Add ApiTraffic to a hapi 21 API: plugin registration, credentials, payloads, error responses, firewall, APM and troubleshooting.

This page as Markdown

Add ApiTraffic to a hapi application to capture every request and response, the calls your app makes to other APIs, and, if you turn them on, firewall decisions and performance spans. Setup is one plugin, registered before the server starts.

Before you begin

  • @hapi/hapi 21, on Node.js 20 or later.
  • An ingest token and a bucket ID. The setup page in ApiTraffic creates the token for you if you are an owner or admin; see API tokens.
  • Outbound HTTPS from your servers to ingest.apitraffic.io and api.apitraffic.io.

Install the package

bash
npm install @apitraffic/node
bash
pnpm add @apitraffic/node
bash
yarn add @apitraffic/node
bash
bun add @apitraffic/node

@apitraffic/node includes the hapi adapter, the firewall and the APM bridge. To install only request capture, see Use the standalone package.

Register the plugin

Register apitraffic.hapi() with server.register() before server.start(). In CommonJS there is no top-level await, so do it inside an async function:

javascript
const Hapi = require('@hapi/hapi');
const apitraffic = require('@apitraffic/node');

const init = async () => {
  const server = Hapi.server({ port: 3000 });

  await server.register(apitraffic.hapi());

  server.route({
    method: 'GET',
    path: '/v1/orders/{orderId}',
    handler: (request) => orders.find(request.params.orderId)
  });

  await server.start();
};

init();

The plugin works through server extensions (onRequest, onPreHandler and onPostResponse), so it covers every route on the server, whether the route is added before or after registration. It is registered with once: true: registering it twice is skipped rather than failing, and each request is reported once.

ES modules and TypeScript

typescript
import Hapi from '@hapi/hapi';
import * as apitraffic from '@apitraffic/node';

const server = Hapi.server({ port: 3000 });
await server.register(apitraffic.hapi());
await server.start();

From an ES module, import apitraffic from '@apitraffic/node' also works. Options are typed by ApiTrafficOptions.

Provide your credentials

With no options, the plugin reads API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET from the environment:

bash
API_TRAFFIC_TOKEN=your-ingest-token
API_TRAFFIC_BUCKET=your-bucket-id

Node does not read .env by itself: start with node --env-file=.env server.js, or make require('dotenv/config'); the first line of your entry file. You can also pass { token, bucket } to apitraffic.hapi(); an option you pass wins over the environment. All options are in SDK configuration.

Verify the connection

Restart the app, then call a route. The setup page in ApiTraffic shows SDK connected - waiting for a request once the SDK connects and Traffic is flowing when the request arrives. Requests made with server.inject() in tests are captured too. If nothing happens, set API_TRAFFIC_DEBUG=true and restart.

What is captured

Each request is reported as the client received it, captured when the response is committed and after its first bytes are written, so capture never delays the response.

  • Request body: request.payload as your app has it when the response is sent, including changes made before responding. Changes made later, for example in an onPostResponse extension, are not reported. When hapi compresses the response, the request is captured when the headers are written.
  • Response body: the payload hapi sent. A stream your handler returns is captured from the bytes sent, up to maxBodyBytes; file downloads served with range support are not copied. Responses with status 204 or 304 have no body.
  • Errors: a thrown error or a Boom error is reported with the error response hapi sent and its status.
  • Status and timing are recorded when the response finishes. A client that disconnects first is reported with status 499, unless it had already received the whole body.
  • Route: the route path hapi matched, such as /v1/orders/{orderId}. Catch-all routes such as /{p*} are grouped by ApiTraffic’s own endpoint detection. See Endpoint handling.

Bodies that cannot be captured safely are replaced with a [body not captured: …] placeholder and the rest of the request is still sent.

Tag and trace requests

javascript
server.route({
  method: 'GET',
  path: '/v1/orders/{orderId}',
  handler: async (request) => {
    apitraffic.tag('customerId', String(request.auth.credentials.id));
    return apitraffic.span('db.query', 'load order', () => orders.find(request.params.orderId));
  }
});

Keys and values follow the tag rules: numbers and booleans are stored as text, long values are cut to 255 characters, and a tag that breaks a rule is dropped and counted in Settings → Dimensions, never thrown into your handler. To add context to errors, tag them in an onPreResponse extension, which runs inside the request. Limits and the full API are in Per-request APIs.

Outbound calls

Calls your app makes with http, https, fetch, axios and similar clients are captured automatically and linked to the request that made them. Turn this off with interceptOutbound: false. See Outbound requests.

Firewall and APM on hapi

Firewall. Already registered by apitraffic.hapi() as an onPreHandler extension, so body rules see the parsed payload. Turn it on by creating a policy under Firewall (WAF) in ApiTraffic. A blocked request gets the status and body your policy defines and never reaches your handler. If your app also registers its own copy of @apitraffic/hapi, the firewall uses that copy’s settings. See Set up the firewall.

APM. Install the OpenTelemetry packages and start the app with the preload:

bash
npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @opentelemetry/auto-instrumentations-node
node -r @apitraffic/node/register ./server.js

See Set up APM for ES modules and apps that already run OpenTelemetry.

Use the standalone package

@apitraffic/hapi is request capture only, with the smallest footprint:

bash
npm install @apitraffic/hapi
javascript
const apiTraffic = require('@apitraffic/hapi');

// inside your async init function, before server.start()
await server.register({
  plugin: apiTraffic.plugin,
  options: {}          // reads API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET; or pass { token, bucket }
});

It exports the same helpers (tag, trace, span, startSpan, flush, shutdown, embedToken), plus deliveryStats() for delivery counters. embedToken() mints with an Administrator API token set as API_TRAFFIC_EMBED_TOKEN, not the ingest token; see Embed tokens. To add the firewall, install @apitraffic/waf and add server.ext('onPreHandler', waf.hapi()) after registering the plugin; see Set up the firewall. For APM, see Set up APM.

Shut down

For a script, worker or test that must exit, call await apitraffic.shutdown() when you are done; the realtime connection otherwise keeps the process alive. In a server, call it after server.stop() in your SIGTERM handler. See Shutting down.

Troubleshooting

Requests are not captured

  • server.register(apitraffic.hapi()) must be awaited before server.start().
  • Restart the process after setting API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET, and check that they reach it.
  • An exclusion may match the request.

SyntaxError: await is only valid in async functions

CommonJS has no top-level await. Put server.register() and server.start() inside an async function, as in the example above.

Test runs with server.inject() never exit

The SDK keeps a realtime connection open. Call await apitraffic.shutdown() in your test teardown.

Outbound calls are missing

See Outbound requests for the cases that are not captured.

  • SDK configuration: Every option, environment variable and per-request API.

  • Node SDK: Features, embed tokens, delivery health and failure behavior.

  • Request log: See and search the requests you just captured.

  • Set up APM: Database, cache and queue spans inside each request.