Connect a LoopBack 4 app

Add ApiTraffic to a LoopBack 4 API as a component: install, register, credentials, what is recorded, firewall, APM and troubleshooting.

This page as Markdown

@apitraffic/loopback is a LoopBack 4 component. Register it once, in your application’s constructor, and every request your REST server serves is recorded and checked by the ApiTraffic firewall, whichever sequence your application uses.

It is @apitraffic/node, the one-install SDK, wired into LoopBack: 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/loopback

yarn

bash
yarn add @apitraffic/loopback

pnpm

bash
pnpm add @apitraffic/loopback

Note: Node 20+ and @loopback/rest 13, 14 or 15 (@loopback/core 5, 6 or 7) required.

Basic Setup

typescript
// src/application.ts
import { ApplicationConfig } from '@loopback/core';
import { RestApplication } from '@loopback/rest';
import { ApiTrafficBindings, ApiTrafficComponent, type ApiTrafficOptions } from '@apitraffic/loopback';

export class MyApplication extends RestApplication {
  constructor(options: ApplicationConfig = {}) {
    super(options);

    this.configure<ApiTrafficOptions>(ApiTrafficBindings.COMPONENT).to({
      token: process.env.API_TRAFFIC_TOKEN,
      bucket: process.env.API_TRAFFIC_BUCKET,
    });
    this.component(ApiTrafficComponent);

    // ...your controllers, sequence, other components
  }
}

Configure the component before registering it: LoopBack builds a component as soon as it is registered. Without a configuration every option falls back to its API_TRAFFIC_* environment variable, so this.component(ApiTrafficComponent) alone is a complete setup.

It works with any basePath, with the default middleware sequence and with the action-based DefaultSequence older applications were generated with, on every REST server the application has, and in an application mounted on an Express app of your own (expressApp.use('/api', lbApp.requestHandler)). Mounted under a path with parameters (/tenants/:tenantId), or through an Express Router, requests are recorded without the route, and the endpoint is worked out from the path.

What is recorded

Every request, including the ones a controller never sees. The component runs ahead of LoopBack’s whole sequence (its CORS handling, middleware, routing, authentication and body parsing), so these are all recorded, with their final status and whatever your code tagged on the way:

  • a request authentication or a middleware rejects or answers itself (401),
  • a request an interceptor rejects (@loopback/authorization’s 403),
  • a request whose parameters fail validation (400, 422),
  • a request no route matches (404), including one outside the REST server’s basePath,
  • a request whose controller throws, as the response LoopBack’s reject action sent.

The route LoopBack matched is sent with each request, controller base path and REST basePath included, so @get('/users/{id}') in a controller with @api({ basePath: '/v1' }) on a server with basePath: '/api' is the endpoint /api/v1/users/{id}. See Endpoint Handling. Requests served by an Express router you mounted with mountExpressRouter(), or by app.static(), are recorded without one, and the endpoint is worked out from the path.

Requests are captured by @apitraffic/express on the REST server’s Express application: exactly what the Express page describes under “What is captured” holds here too.

Tagging, tracing and spans

The helpers work anywhere a request is being handled: controllers, services, repositories, interceptors, middleware and sequence actions.

typescript
import { Interceptor } from '@loopback/core';
import { RestBindings } from '@loopback/rest';
import { tag } from '@apitraffic/loopback';

export const accountTag: Interceptor = async (invocationCtx, next) => {
  const request = await invocationCtx.get(RestBindings.Http.REQUEST);
  // Searchable on this request, whether or not it gets any further.
  tag('accountId', String(request.headers['x-account-id']));
  return next();
};
typescript
import { repository } from '@loopback/repository';
import { span, trace } from '@apitraffic/loopback';
import { UserRepository } from '../repositories';

export class UserService {
  constructor(@repository(UserRepository) private users: UserRepository) {}

  async findOne(id: string) {
    trace(`Loading user ${id}`);
    return span('db.query', 'load user', () => this.users.findById(id));
  }
}

Outside a request (a cron job, a queue consumer) 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.stop() (which the application generated by lb4 calls on SIGTERM and SIGINT when shutdown.signals is set) delivers what was captured and releases the SDK’s timers and connections once the last application using the component has stopped. The REST server has stopped by then, so every request it served has been reported. An application started again afterwards sets the SDK up again and is recorded as before; the APM bridge is not restarted.

Firewall

The ApiTraffic firewall is mounted by the component, 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 twice on a request at most, and counts it once:

  • ahead of the sequence, right after the request is recorded, so a request it blocks there never reaches LoopBack’s middleware, routing or authentication;
  • once LoopBack has parsed the body, so body rules see it: with the default middleware sequence, between parameter parsing and the method, ahead of every interceptor; with the action-based DefaultSequence, as a global interceptor in the group apitraffic.firewall (list it first in your global interceptor order to run it ahead of the others). It is evaluated again there only if it had no body before.

Blocked requests are recorded with the policy’s status and tagged waf.*.

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
this.configure<ApiTrafficOptions>(ApiTrafficBindings.COMPONENT).to({ 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 LoopBack and your datasource connectors do. Preload it:

bash
node -r @apitraffic/loopback/register .

or import it on the very first line of src/index.ts, ahead of every other import:

typescript
import '@apitraffic/loopback/register';

(Not register() from @apitraffic/loopback itself: importing the component loads LoopBack’s REST stack, which then loads before the bridge can patch it.)

If your application already configures OpenTelemetry (with @loopback/opentelemetry, say), 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

An option passed in code wins over the environment.

The options are typed: with configure(), a value of the wrong type is a compile error, and so is a misspelled key in an object literal.

Embed tokens

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

typescript
import { get } from '@loopback/rest';
import { embedToken } from '@apitraffic/loopback';

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

Troubleshooting

No requests are recorded

Check that this.component(ApiTrafficComponent) runs in the application you start, and that API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET are set. Set debug: true to see what the SDK is doing.

The options I configured are ignored

this.configure(ApiTrafficBindings.COMPONENT) has to come before this.component(ApiTrafficComponent): the component reads its configuration when it is registered.

Tags set in a cron 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.

I already mount @apitraffic/node with expressMiddleware()

Remove it when you register the component. Left in place it does no harm (each request is still recorded and evaluated once), but the component records more of your traffic than it can: expressMiddleware() runs inside the sequence, after a request outside the basePath has already been answered.

Sample Application

A complete working example is available in the GitHub repository.