nest-profiler-http
Record outgoing HTTP client calls in the HTTP Client panel.
@eleven-labs/nest-profiler-http
@eleven-labs/nest-profiler-http captures outgoing HTTP requests and displays them in a dedicated HTTP Client panel. It is client-agnostic: it owns the HttpRequestEntry contract, the collector, the HttpProfilerRecorder and an HttpInstrumentation interface, and never depends on any HTTP-client library. It ships two opt-in, subpath-isolated adapters — axios and fetch — that both capture request and response bodies safely, and you pick exactly which client(s) to instrument. Nothing is patched unless you select it, and you can bring your own client the same way.

Installation
pnpm add @eleven-labs/nest-profiler-http@alpha
# only if you select the axios adapter — your app already owns these:
pnpm add @nestjs/axios axiosThere is no stable release yet — install every
@eleven-labs/nest-profiler*package with the@alphadist-tag (@latestresolves to nothing).
Optional peer dependency: axios ^1.0.0 (type-only, used by the /axios adapter). fetch is a Node ≥ 22 built-in and needs no dependency. This package never imports @nestjs/axios — that is your application's dependency.
Selecting clients
Import each adapter from its own subpath and list it in instrumentations. Nothing is instrumented unless it appears in the list.
import { ConditionalModule } from '@nestjs/config';
import { HttpCollectorModule } from '@eleven-labs/nest-profiler-http';
import { AxiosInstrumentation } from '@eleven-labs/nest-profiler-http/axios';
import { FetchInstrumentation } from '@eleven-labs/nest-profiler-http/fetch';
const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';
@Module({
imports: [
ConditionalModule.registerWhen(
HttpCollectorModule.forRoot({
instrumentations: [AxiosInstrumentation, FetchInstrumentation],
captureResponseBody: true,
}),
isProfilerEnabled,
),
],
})
export class AppModule {}Each adapter lives on its own subpath (/axios, /fetch), so importing one never loads another's dependency. The root barrel exports only the client-agnostic API.
Enabling / disabling — gate the collector with
ConditionalModule.registerWhen(..., isProfilerEnabled)as shown, so it loads only whenPROFILER_ENABLEDis on. Wire the coreProfilerModuleonce at the root — the recommended setup bundles the root-level profiler modules into a singleProfilingModulebehind aConditionalModulegate (see Enabling and disabling the profiler and the example app). A top-levelenabledoption is also supported as an alternative.
How each adapter finds requests
AxiosInstrumentation(/axios) — auto-discovers every axios instance in the DI container viaDiscoveryService:@nestjs/axiosHttpService(including each per-featureHttpModule/HttpModule.register(), which build distinct instances) and bare axios instances provided directly. NoaxiosRefwiring, no@nestjs/axiosimport. Just injectHttpServicein your services as usual — requests are captured automatically. Axios instances created outside DI (a bareaxios.create()held in a private field, a third-party library's internal client) aren't discoverable — record those with a custom instrumentation (below).FetchInstrumentation(/fetch) — patchesglobalThis.fetchonce. A single global hook covers every caller.
Other clients (got, undici, superagent…)? There is no
node:httpcatch-all: instrument them with a small customHttpInstrumentationusing the client's own hooks (see Bring your own HTTP client). Going through the client's native API captures full request and response bodies safely — which a genericnode:httphook cannot do for response bodies.
Bring your own HTTP client
For an ad-hoc call, inject HttpProfilerRecorder and call capture() — it applies your capture options (headers/body) and masks sensitive headers, so a custom client shows the same detail in the panel:
import { HttpProfilerRecorder } from '@eleven-labs/nest-profiler-http';
@Injectable()
export class WeatherService {
constructor(private readonly recorder: HttpProfilerRecorder) {}
async getForecast() {
const url = 'https://api.weather.example.com/forecast';
const startedAt = Date.now();
const res = await fetch(url, { headers: { accept: 'application/json' } });
const body = await res.json();
this.recorder.capture({
method: 'GET',
url,
startedAt,
duration: Date.now() - startedAt,
statusCode: res.status,
responseHeaders: res.headers, // fetch `Headers` (and `Map`) are supported
responseBody: body,
});
return body;
}
}For a reusable integration, implement HttpInstrumentation — a NestJS provider with install(recorder) — and add it to instrumentations. It can inject ModuleRef, config, etc. This is exactly how the bundled adapters work. Example, instrumenting got:
import { Injectable } from '@nestjs/common';
import type { HttpInstrumentation, HttpProfilerRecorder } from '@eleven-labs/nest-profiler-http';
import got from 'got';
@Injectable()
export class GotInstrumentation implements HttpInstrumentation {
install(recorder: HttpProfilerRecorder): void {
got.extend({
hooks: {
beforeRequest: [
(options) => {
(options as { _start?: number })._start = Date.now();
},
],
afterResponse: [
(response) => {
const started = (response.request.options as { _start?: number })._start ?? Date.now();
recorder.capture({
method: response.request.options.method,
url: response.requestUrl.toString(),
startedAt: started,
duration: Date.now() - started,
statusCode: response.statusCode,
responseHeaders: response.headers,
responseBody: response.body,
});
return response;
},
],
},
});
}
}
// HttpCollectorModule.forRoot({ instrumentations: [GotInstrumentation] });Use record(entry) instead of capture(input) if you have already built a final HttpRequestEntry and want to bypass the capture options. The example API swaps its whole ArticleGateway between the axios and fetch adapters with HTTP_CLIENT=axios|fetch — run it with HTTP_CLIENT=fetch to see the fetch adapter capturing the same calls.
Options
HttpCollectorModule.forRoot(options) accepts:
| Option | Default | Description |
|---|---|---|
instrumentations | [] | The adapters to install (AxiosInstrumentation, FetchInstrumentation, …). Nothing is instrumented unless listed. |
slowThreshold | 300 | Calls at/above this duration (ms) are tagged slow. |
nPlusOneThreshold | 2 | Identical calls repeated ≥ N in one request are tagged n-plus-one. |
chattyThreshold | 10 | A request making ≥ N outgoing calls is tagged chatty. |
largePayloadThreshold | 1048576 | A call whose payload reaches this size (bytes) is tagged large-payload. 0 disables. |
slowSeverity | warning | Severity of the slow tag ('info' | 'warning' | 'danger'). |
nPlusOneSeverity | danger | Severity of the n-plus-one tag. |
chattySeverity | warning | Severity of the chatty tag. |
largePayloadSeverity | warning | Severity of the large-payload tag. |
error | 5xx | What counts as a failed call: it threw, or answered ≥ 500. A 404 from an API you call is an answer, not a failure — pass { httpStatus: 400 } to count it. See What counts as an error. |
captureRequestHeaders | true | Capture (and mask) outgoing request headers. |
captureRequestBody | false | Capture request body for non-GET/HEAD requests. |
captureResponseHeaders | true | Capture (and mask) response headers. |
captureResponseBody | false | Capture response body — can be large. |
maskHeaders | [] | Extra header names to redact (merged with the defaults). |
What it collects
For each outgoing request: method, url, statusCode, duration, startedAt, optional error, and (per options) request/response headers and bodies.
Toolbar badge
Request count (e.g. 3). When errors are present: 3 (1 err).
Panel behaviour
The HTTP Client panel lets you expand each row to inspect request/response headers and bodies. That behaviour ships as a compiled, same-origin browser bundle (http.js) that the module registers with the profiler automatically — there is nothing to configure, and the templates carry no inline JavaScript. It is a reference implementation of the Extending the UI with JavaScript pattern, reusing the core window.NestProfiler runtime.