
@apitraffic/loopback is ApiTraffic for LoopBack 4, as a LoopBack component. Register it once in your application's constructor and every request your REST server serves is recorded - request and response, headers, bodies, timing, the route LoopBack matched, and the outbound HTTP calls made while serving it - and checked by the ApiTraffic firewall, whichever sequence your application uses.
It is @apitraffic/node, the one-install SDK, wired into LoopBack. It contains no capture, firewall or instrumentation logic of its own: requests are recorded by @apitraffic/express on the REST server's Express application and checked by @apitraffic/waf, exactly as in a plain Express application. What actually runs - the firewall, redaction, exclusions, span capture - is switched on from your ApiTraffic account, not in code.
Contents
Which package should I use?
On LoopBack 4, use this package. It is @apitraffic/node placed where LoopBack needs it: capture on the REST server's Express application, ahead of the whole sequence, and the firewall both there and again once LoopBack has parsed the body. If you currently mount @apitraffic/node with expressMiddleware(), remove it when you register the component - it runs inside the sequence and misses requests answered before it.
For other frameworks, see the "Which package should I install?" table in the @apitraffic/node README.
Requirements
- Node.js 20 or later.
- LoopBack 4:
@loopback/rest 13, 14 or 15 with @loopback/core 5, 6 or 7 (peer dependencies).
- Outbound HTTPS from your servers to
ingest.apitraffic.io (telemetry) and api.apitraffic.io (settings and live configuration). Live configuration changes (redaction rules, exclusions, firewall policy) are pushed over a realtime connection to Ably, so also allow its realtime hosts; if that connection is blocked, live updates wait for a restart and nothing else is affected.
- An ingest token and a bucket ID from your ApiTraffic account. See API tokens.
Installation
npm install @apitraffic/loopback
pnpm add @apitraffic/loopback
yarn add @apitraffic/loopback
@apitraffic/node comes with it as a dependency. LoopBack itself stays your own dependency.
Quick start
Set your credentials in the environment:
export API_TRAFFIC_TOKEN=your-ingest-token
export API_TRAFFIC_BUCKET=your-bucket-id
Register ApiTrafficComponent in your application's constructor:
import { ApplicationConfig } from '@loopback/core';
import { RestApplication } from '@loopback/rest';
import { ApiTrafficComponent } from '@apitraffic/loopback';
export class MyApplication extends RestApplication {
constructor(options: ApplicationConfig = {}) {
super(options);
this.component(ApiTrafficComponent);
}
}
To pass options in code, configure the component on ApiTrafficBindings.COMPONENT (the binding key components.ApiTrafficComponent) before registering it - LoopBack builds a component as soon as it is registered:
import { ApiTrafficBindings, ApiTrafficComponent, type ApiTrafficOptions } from '@apitraffic/loopback';
this.configure<ApiTrafficOptions>(ApiTrafficBindings.COMPONENT).to({
token: process.env.API_TRAFFIC_TOKEN,
bucket: process.env.API_TRAFFIC_BUCKET,
});
this.component(ApiTrafficComponent);
Set shutdown.signals in your application config (as the lb4 generator does) so what was captured is delivered when the process is stopped (see Lifecycle).
Where it mounts
There is nothing to order by hand. When the application initializes, the component finds every REST server bound to it and mounts on each:
- Capture, then the firewall, at the head of the REST server's Express application - right after Express' own query parser and request setup, ahead of LoopBack's CORS handling, middleware, routing, authentication and body parsing. The server builds that Express application lazily (at
start(), or when it is mounted on an Express app of your own), and the component waits for it rather than building it early, so your basePath is honoured.
- The route LoopBack matched is declared on the request from inside the sequence, as middleware after
findRoute, ahead of authentication and parameter parsing.
- The firewall, once more, after LoopBack has parsed the body: 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 works with any basePath, with both kinds of sequence, on every REST server the application has, and in an application mounted on an Express app of your own (expressApp.use('/api', lbApp.requestHandler)). A REST server whose Express application cannot be reached gets capture at the head of its sequence instead. An application without a REST server (a migration script, a CLI) is left alone and never sets the SDK up.
Configuration
Options are configured on ApiTrafficBindings.COMPONENT, or set as environment variables. You can mix the two.
Precedence: an option passed in code wins over its environment variable, which wins over the built-in default. With every option in the environment, this.component(ApiTrafficComponent) alone is a complete setup. On top of that, settings from your ApiTraffic account - redaction rules, exclusions, the firewall policy, APM capture switches - are loaded at startup and pushed to every connected server in real time, without a restart or a deploy.
A few settings combine differently:
| Firewall on or off | Off if either waf: false is passed or API_TRAFFIC_WAF_ENABLED is exactly false or 0, or off in any case. This variable does not use the switch parser below: FALSE, no or a value with spaces leaves the firewall on. Neither can turn on a firewall your account has not enabled. |
waf.offlineWafMode | API_TRAFFIC_OFFLINE_WAF_MODE wins over the code option, so you can change it during an incident without a deploy. Only exactly closed (lowercase) means closed: any other non-empty value, such as CLOSED, still overrides the option and fails open. |
captureStatementParams, captureRedisKeys | On if your code or environment turns it on, or the bucket's APM Capture settings do. |
captureCallSite | The bucket's APM Capture setting applies unless you set it in code or the environment. Set locally it can only turn code location on for the SDK's own spans: a local false does not override a bucket that has it on. The APM bridge reads API_TRAFFIC_CAPTURE_CALL_SITE (or its own captureCallSite option) itself, so a false there does turn it off for OpenTelemetry spans. |
captureStackDepth | The bucket's APM Capture setting applies unless you set it in code or the environment, which then wins. |
interceptOutbound | A blank value ("", null, an empty variable) turns outbound capture off. For the other switches, limits, sample rates, token, bucket and accountSid, a blank value counts as not set (apiHost and ingestHost fall back only when empty; a value of spaces is used as given). Two exceptions: passing debug at all, even as undefined, overrides API_TRAFFIC_DEBUG, and a blank projectRoot in code skips API_TRAFFIC_PROJECT_ROOT. |
Switches and numeric limits also accept strings (true/false, yes/no, on/off, 1/0; whole numbers for byte caps), so { captureSpans: process.env.CAPTURE_SPANS } works. An invalid value is reported once on the console and ignored. debug and strict are not parsed from strings in code - pass true or false - and API_TRAFFIC_DEBUG turns debug on only when it is exactly true (any case).
Options
token | API_TRAFFIC_TOKEN | string | - | Required. Ingest token from your ApiTraffic account. |
bucket | API_TRAFFIC_BUCKET | string | - | Required. ID of the bucket captured traffic is sent to. |
waf | API_TRAFFIC_WAF_ENABLED | false or object | unset | Local override for the firewall. false refuses it whatever the dashboard says; an object passes settings to @apitraffic/waf (and enabled: false in it turns the firewall off too). Normally left unset. |
debug | API_TRAFFIC_DEBUG | boolean | false | Log SDK activity to the console. |
strict | - | boolean | false | Makes the base client's setup() throw on misconfiguration (missing token or bucket) instead of logging. This package runs setup in the background and catches that error, so strict does not stop your app from booting, and the missing-token message is then printed only when API_TRAFFIC_DEBUG is set. Leave it off. |
accountSid | API_TRAFFIC_ACCOUNT_SID | string | learned from settings | Only needed by a process that mints embed tokens without serving requests. |
apiHost | API_TRAFFIC_API_HOST | string | api.apitraffic.io | Settings and configuration host. |
ingestHost | API_TRAFFIC_INGEST_HOST | string | ingest.apitraffic.io | Host captured traffic is sent to. |
interceptOutbound | API_TRAFFIC_INTERCEPT_OUTBOUND | switch | true | Capture outbound http, https and fetch calls. |
captureFetch | API_TRAFFIC_CAPTURE_FETCH | switch | true | Capture outbound global fetch (needs interceptOutbound). |
redactSensitiveKeys | API_TRAFFIC_REDACT_SENSITIVE_KEYS | switch | true | Mask credential-shaped body keys and query parameters. |
maxBodyBytes | API_TRAFFIC_MAX_BODY_BYTES | bytes | 524288 (512 KB) | Largest request or response body sent; a larger one is cut to the cap and ends with …[truncated by ApiTraffic: N bytes total]. |
maxRedactBodyBytes | API_TRAFFIC_MAX_REDACT_BODY_BYTES | bytes | 1048576 (1 MB) | Largest body the SDK redacts; a larger one becomes a placeholder first. |
maxFetchBodyLength | API_TRAFFIC_MAX_FETCH_BODY_LENGTH | bytes | 524288 (512 KB) | Largest outbound body captured. |
requestSampleRate | API_TRAFFIC_REQUEST_SAMPLE_RATE | 0–1 | 1 | Fraction of requests sent. 5xx responses are always sent. |
ingestBatch | API_TRAFFIC_INGEST_BATCH | switch | true | Batch payloads when your account and the ingest host support it. |
ingestGzip | API_TRAFFIC_INGEST_GZIP | switch | true | Compress those batches when your account asks for it. |
captureSpans | API_TRAFFIC_CAPTURE_SPANS | switch | true | Master switch for spans and per-request span rollups. |
captureStatements | API_TRAFFIC_CAPTURE_STATEMENTS | switch | true | Include normalized SQL/command text in spans. |
captureStatementParams | API_TRAFFIC_CAPTURE_STATEMENT_PARAMS | switch | false | Also keep statements as issued, with bind values (your users' data). |
captureRedisKeys | API_TRAFFIC_CAPTURE_REDIS_KEYS | switch | false | Include the concrete Redis key, unmasked. |
captureCallSite | API_TRAFFIC_CAPTURE_CALL_SITE | switch | true (bucket setting) | Record the application code location that started each span. |
captureStackDepth | API_TRAFFIC_CAPTURE_STACK_DEPTH | whole number ≥ 0 | 10 (bucket setting) | Application frames kept per span. |
projectRoot | API_TRAFFIC_PROJECT_ROOT | string | detected | Directory code locations are reported relative to. |
redisKeyPatterns | - | array of strings or RegExps | [] | Redis key patterns the automatic masking cannot infer. |
maxSpansPerRequest | API_TRAFFIC_MAX_SPANS_PER_REQUEST | count | 500 | Spans kept per request. |
spanSampleRate | API_TRAFFIC_SPAN_SAMPLE_RATE | 0–1 | 1 | Fraction of requests that keep full span detail. |
maxSpanMetaBytes | API_TRAFFIC_MAX_SPAN_META_BYTES | bytes | 2048 | Size cap on each span's metadata, after redaction. |
Delivery to ApiTraffic honours HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and NO_PROXY. The configuration reference covers the long tail: firewall options, the APM preload's own options, offline definitions and server labels.
What gets captured
Every request the application serves, including the ones a controller never sees, with its 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(), by app.static(), or under a mount path with parameters (/tenants/:tenantId) are recorded without a route, and the endpoint is worked out from the path.
- Inbound: method, URL, headers, request and response bodies, status and timing. Bodies over
maxBodyBytes (512 KB) are cut to that size and marked as truncated, and bodies over maxRedactBodyBytes (1 MB) are replaced by a placeholder.
- Outbound: calls made with
http, https and global fetch while serving a request, linked to it. See Outbound requests.
- Sampling:
requestSampleRate sends a fraction of requests; server errors are always sent.
Capture is @apitraffic/express, so what the Express page describes under "What is captured" holds here too.
Data security
Nothing here needs a code change: it is configured in your ApiTraffic account and pushed to every connected server.
- Client-side redaction. Redaction rules are applied in your process before anything leaves it. Nothing is sent until your account's rules have loaded, and a request whose redaction cannot be completed is dropped, never sent unredacted.
- Sensitive key masking. Credential headers are always masked. With
redactSensitiveKeys (on by default), credential-shaped body keys and query parameters (password, token, secret, api_key and similar) are masked too.
- Exclusions. Exclusions stop matching requests from being sent at all.
Firewall
The ApiTraffic firewall is mounted by the component, and whether it runs is decided in your ApiTraffic account: nothing is loaded until a policy is live, and a policy change reaches every server without a deploy. It runs at most twice on a request, and counts it once against rate limits:
- 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, ahead of your interceptors and controller. It is evaluated again there only if it had no body the first time.
Blocked requests are still recorded, with the policy's status and waf.* tags.
To refuse it locally whatever the dashboard says, set waf: false or API_TRAFFIC_WAF_ENABLED=false:
this.configure<ApiTrafficOptions>(ApiTrafficBindings.COMPONENT).to({ waf: false });
Use the option when your change process requires that anything able to reject production traffic ships through a deploy; use the variable as an incident kill switch, which works even when ApiTraffic is unreachable. See Firewall setup.
APM
The APM bridge records database, cache, queue, GraphQL, gRPC and fetch/undici spans inside each request. OpenTelemetry instruments libraries by patching them as they are loaded, so it has to start before LoopBack and your datasource connectors do - by the time a component is registered, that moment has passed. So it needs a preload.
Install @apitraffic/apm, which brings the OpenTelemetry packages it needs:
npm install @apitraffic/apm
Then preload the bridge:
node -r @apitraffic/loopback/register .
or import it on the very first line of src/index.ts, ahead of every other import:
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.
@apitraffic/loopback/register is @apitraffic/node/register, re-exported so it resolves from an application that installs only this package (with pnpm, @apitraffic/node is not resolvable from your app). It reads its configuration from the API_TRAFFIC_* environment variables and never throws: if the OpenTelemetry packages are missing or fail to start, APM is skipped with a one-line reason and your application boots normally.
The preload is for applications with no other tracing. If yours already configures OpenTelemetry (with @loopback/opentelemetry, say), skip it and add ApiTrafficSpanProcessor from @apitraffic/apm to that provider; with Datadog or New Relic loaded the preload steps aside on its own. See Set up APM.
Tagging, tracing and spans
The helpers work anywhere a request is being handled: controllers, services, repositories, interceptors, middleware and sequence actions. Outside a request (a cron job, a queue consumer) they do nothing, and they never throw.
Tagging
tag(key: string, value: string) makes a request searchable, for example by account or user ID:
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);
tag('accountId', String(request.headers['x-account-id'] ?? 'none'));
return next();
};
Keys are 1–64 characters (a letter first, then letters, digits, _, . or -); the prefixes redaction., apitraffic. and at. are reserved and dropped, and waf. is the firewall's own namespace (such a key is stored among the firewall's tags), so do not use it. Values are cut to 255 characters, and a request holds at most 32 tag keys. A tag that breaks a rule is dropped, never thrown. See Tags.
Tracing
trace(content: string) is console.log for production: a timestamped note on the request (up to 255 characters, 250 per request; a longer message is dropped, not cut).
import { trace } from '@apitraffic/loopback';
trace('cart total exceeded the free-shipping threshold');
Spans
span(type, name, fn, meta?) runs fn, times it and records it on the request's waterfall, returning what fn returns; time() is the same function. startSpan(type, name, meta?) returns a handle to end() for work that does not fit one function, and addSpan() records a span you built yourself.
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));
}
}
type is one of db.query, redis, cache, http.out, queue, rpc, graphql or custom. getContext() and getRequestManager() expose the current request's context for advanced use; most applications never need them. See Per-request APIs.
Embed tokens
The embeddable request log (and embedded reports and dashboards) authenticate with a short-lived JWT that locks in the bucket, the filters and the request detail tabs the embed may show. It is minted on your server with an Administrator API token with no access scopes, set as API_TRAFFIC_EMBED_TOKEN (the ingest token the SDK reports with cannot mint embed tokens), and this package mints it:
import { inject } from '@loopback/core';
import { get } from '@loopback/rest';
import { authenticate } from '@loopback/authentication';
import { SecurityBindings, UserProfile, securityId } from '@loopback/security';
import { embedToken } from '@apitraffic/loopback';
export class EmbedController {
@authenticate('jwt')
@get('/embed/token')
async token(@inject(SecurityBindings.USER) user: UserProfile) {
const { token, expiresAt } = await embedToken({
filters: { tags: { customerId: user[securityId] } },
ttl: 900,
});
return { token, expiresAt };
}
}
The host, bucket and account come from the SDK you already configured and the token from API_TRAFFIC_EMBED_TOKEN, so a call names only what is specific to this embed. Everything is optional, except reportSid or dashboardSid for a report or dashboard embed:
bucketSid | The bucket the SDK reports to (request log only) | The bucket the embed is scoped to. A report or dashboard keeps its own scope unless you pass one. |
view | 'request-log' | 'request-log', 'report' (with reportSid) or 'dashboard' (with dashboardSid). |
filters | {} | environmentSid, criteria, streamViewSid, tags (request log) or filter, environmentSid, tags (report and dashboard) - signed into the token, so the browser cannot widen them. tags ({ customerId: id }) is written into the criteria (for a report or dashboard, the filter) safely; if you build criteria yourself, put values in double quotes through escapeCriteriaValue(). |
timeRange / timeRanges | The report's own / all | Report and dashboard embeds: the range the embed opens with ({ relative: 'last_7d' }) and the ranges the viewer may pick. |
ttl | 3600 | Token lifetime in seconds (60–86400). |
theme | 'light' | 'light', 'dark' or 'auto'. |
tabs | The API's default set | Request detail tabs this embed may show. Request log only: the API refuses it for a report or dashboard. |
features | {} | copyRequest, includeBodies. Request log only: the API refuses it for a report or dashboard. |
accountSid | From your SDK settings | Only needed in a process that does not serve requests through ApiTraffic. Also read from API_TRAFFIC_ACCOUNT_SID. |
token / apiHost | API_TRAFFIC_EMBED_TOKEN / the SDK's host | Override the credential or host for one call. |
Data behind a tab you do not grant is never returned to the browser - the tabs are an access boundary, not a display setting.
This is the one call in the package that rejects rather than going quiet. A token has a single useful outcome, and it is produced inside your own await, where a failure is a 500 on that route instead of a broken embed with nothing to explain it.
API_TRAFFIC_EMBED_TOKEN is a read credential. An Administrator API token can mint embed tokens for any bucket in its account, with any filters or none, so whoever holds it can read all of the account's captured traffic. Keep it in a secret store (never in client-side code, images or a repository), set it only on servers that mint embed tokens, and rotate it immediately if it may have been exposed.
Lifecycle: shutdown, flush and stats
app.stop() delivers what was captured and releases the SDK's timers, realtime connection and outbound hooks once the last application using the component has stopped. The component's lifecycle observer stops after the REST servers, so every request they 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.
The SDK installs no signal handlers of its own. Applications generated by lb4 call app.stop() on SIGTERM and SIGINT when shutdown.signals is set:
export async function main() {
const app = new MyApplication({
rest: { port: 3000 },
shutdown: { signals: ['SIGTERM', 'SIGINT'] },
});
await app.start();
return app;
}
@apitraffic/node's lifecycle functions are re-exported for the cases LoopBack's own lifecycle does not cover:
flush({ timeoutMs }) delivers what has been captured without stopping (default timeout 2 s), for example before a serverless function is frozen. Anything not delivered in time stays queued.
shutdown({ flushTimeoutMs }) stops capturing, delivers what is queued and releases everything. You do not need it when the application is stopped through app.stop().
stats() returns delivery counters (pending, queued, delivered, recovered, dropped, breakerOpen, …), for a health endpoint or your own monitoring.
All three never throw.
TypeScript
The package ships its own declarations. The options are typed: ApiTrafficOptions lists every option the SDK accepts; with configure<ApiTrafficOptions>() a value of the wrong type is a compile error, and so is a misspelled key in an object literal (unlisted keys are still passed through at runtime).
import { ApiTrafficBindings, type ApiTrafficOptions } from '@apitraffic/loopback';
this.configure<ApiTrafficOptions>(ApiTrafficBindings.COMPONENT).to({
debug: process.env.NODE_ENV !== 'production',
});
Exported values: ApiTrafficComponent and ApiTrafficBindings (with COMPONENT, a BindingKey<ApiTrafficComponent> for components.ApiTrafficComponent), plus the helpers. Exported types: ApiTrafficOptions, WafConfigureOptions, ApmOptions, RegisterResult, SpanHandle, SpanMeta, EmbedTokenOptions and EmbedTokenResult.
Failure behaviour
The SDK is built so that it can never be the reason your application fails.
- Mounting never throws. A REST server the component cannot mount on is left serving, unrecorded.
- A missing or invalid token disables telemetry and logs a clear message. Your application still boots.
- If ApiTraffic is unreachable, requests are served normally. A short outage is absorbed by an in-memory retry queue; a longer one sheds data rather than growing without bound. See If ApiTraffic is unreachable.
- If the realtime connection fails, you lose live configuration updates and nothing else.
- Nothing is sent before your account's client-side redaction rules have loaded.
tag() and trace() never throw into your handlers, and every helper does nothing outside a request.
Want the app to refuse to boot without credentials? Check API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET yourself before you start the server. strict: true does not do it here: setup runs in the background and its error is caught.
Troubleshooting
Start with debug logging: set API_TRAFFIC_DEBUG=true (or pass debug: true) and restart. The component's own mounting messages are printed when API_TRAFFIC_DEBUG is set. A missing token is logged even without debug.
- No requests are recorded. Check that
this.component(ApiTrafficComponent) runs in the application you start, and that API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET reach the process (Node does not load .env on its own). The setup page in ApiTraffic shows whether the SDK has connected.
- 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.
- Nothing arrives, and the SDK cannot connect. Allow outbound HTTPS to
ingest.apitraffic.io and api.apitraffic.io. Behind a proxy, set HTTPS_PROXY.
- Bodies are missing or show a placeholder. A body larger than
maxBodyBytes (512 KB) is cut, and one larger than maxRedactBodyBytes (1 MB) is replaced by a placeholder; raise the caps if you need more. See Troubleshooting.
- Tags set in a cron job or queue consumer are missing. Tags, traces and spans belong to an HTTP request; outside one the helpers do nothing.
- I already mount
@apitraffic/node with expressMiddleware(). Remove it. Left in place it does no harm (each request is still recorded and evaluated once), but it runs inside the sequence, after a request outside the basePath has already been answered.
- "the installed @apitraffic/waf has no … firewall". A live policy is not being enforced because an older
@apitraffic/waf is installed alongside. Update it to the version @apitraffic/node depends on, or remove the duplicate.
- APM shows no spans. The preload has to run before LoopBack and your connectors load, and
@apitraffic/apm must be installed next to it. See Set up APM.
More symptoms and fixes are in the troubleshooting guide.
Sample application
The setup guide walks through a complete application. The ApiTraffic source (not the npm package) also includes a working LoopBack 4 application in examples/basic. It registers ApiTrafficComponent with credentials from the environment and sets shutdown.signals.
export API_TRAFFIC_TOKEN=your-ingest-token
export API_TRAFFIC_BUCKET=your-bucket-id
npm install
npm start
It listens on port 4000: try /, /users/42 (a route parameter and a span), /outbound (an outbound call) and /admin (a request an interceptor rejects), then open the bucket's traffic stream in ApiTraffic.
Documentation and related packages
Contributing, changelog and license
Follow @apitraffic/loopback on npm to stay updated on new releases.
Follow us on our Blog and on X.
Chat with the ApiTraffic team and other members on Discord and follow our tutorials and other videos on YouTube.



How to contribute
Here are some ways you can help improve ApiTraffic:
- Give ApiTraffic a try and share your feedback to help make ApiTraffic even better for you. Feel free to reach out to us on Discord!
- Join our Discord to connect with other members, share your experiences, and learn from one another.
- Report bugs or request features by emailing support@apitraffic.io or asking on Discord.