HTTP client profiling
Capture outgoing HTTP requests and inspect them in the HTTP Client panel - with the axios and fetch adapters or any client of your own.
This tutorial shows how to profile outgoing HTTP requests in a NestJS application. The HTTP Client panel lives in @eleven-labs/nest-profiler-http and is client-agnostic: select one or both of the bundled adapters (axios, fetch), or wire any other client (undici, got…) yourself through the HttpProfilerRecorder or a custom HttpInstrumentation. Nothing is instrumented unless you select it.
Prerequisites
@eleven-labs/nest-profilerinstalled and configured- For the axios adapter only:
@nestjs/axiosandaxiosinstalled (fetch is a Node ≥ 22 built-in)
Step 1 - Install the package
pnpm add @eleven-labs/nest-profiler-http@alpha
# only if you select the axios adapter:
pnpm add @nestjs/axios axiosStep 2 - Register the module and select your client(s)
Import each adapter from its subpath and list it in instrumentations. The axios adapter auto-discovers every HttpService in your app (via DiscoveryService), so there is no axiosRef to wire — even across several feature modules that each create their own HttpService.
import { Module } from '@nestjs/common';
import { ConditionalModule } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';
import { HttpCollectorModule } from '@eleven-labs/nest-profiler-http';
import { AxiosInstrumentation } from '@eleven-labs/nest-profiler-http/axios';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';
@Module({
imports: [
HttpModule,
ConditionalModule.registerWhen(
HttpCollectorModule.forRoot({ instrumentations: [AxiosInstrumentation] }),
isProfilerEnabled,
),
],
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}Using fetch instead of (or alongside) axios? Add FetchInstrumentation (/fetch) to the list.
For any other client (got, undici, superagent…), write a small custom instrumentation using the
client's own hooks — see Wire your own HTTP client below. Going
through the client's native API captures full request and response bodies safely.
Step 3 - Use HttpService in a service
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class PostsService {
constructor(private readonly http: HttpService) {}
async findAll(): Promise<unknown[]> {
const { data } = await firstValueFrom(
this.http.get('https://jsonplaceholder.typicode.com/posts?_limit=5'),
);
return data;
}
}Step 4 - Test it
Start your application and trigger a request that makes an outgoing HTTP call:
# Cache miss - triggers an outgoing call to the external API
curl -i http://localhost:3000/postsCopy the X-Debug-Token from the response headers, open /_profiler/{token}, and click the HTTP Client tab.
You will see each outgoing request with:
- Method -
GET,POST,PUT, etc. - URL - full URL including query parameters
- Status - HTTP response status code (e.g.,
200,404) - Duration - round-trip time in milliseconds
The toolbar badge displays {n}req - for example 3req when three outgoing requests were made during the incoming request.
Combining with cache profiling
A common pattern is to fetch from an external API and cache the result. With both CacheCollectorModule and HttpCollectorModule registered, you can compare the two panels across requests:
# First call - Cache MISS, outgoing call fires → HTTP Client tab shows 1req
curl http://localhost:3000/posts
# Second call - Cache HIT, no outgoing call → HTTP Client tab shows 0req
curl http://localhost:3000/postsWire your own HTTP client
Using a client without a bundled adapter (undici, got…)? Feed the panel yourself by injecting HttpProfilerRecorder - exported and provided by HttpCollectorModule. Time your call and pass the raw request/response material to recorder.capture(...): it applies your capture options (request/response headers + body) and masks sensitive headers for you, so a custom client shows the same detail in the panel as the bundled adapters. The collector registered by HttpCollectorModule renders the panel, so no other import is needed.
import { Injectable } from '@nestjs/common';
import { HttpProfilerRecorder } from '@eleven-labs/nest-profiler-http';
@Injectable()
export class WeatherService {
constructor(private readonly recorder: HttpProfilerRecorder) {}
async getForecast(): Promise<unknown> {
const url = 'https://api.weather.example.com/forecast';
const requestHeaders = { accept: 'application/json' };
const startedAt = Date.now();
const response = await fetch(url, { headers: requestHeaders });
const body = await response.json();
this.recorder.capture({
method: 'GET',
url,
startedAt,
duration: Date.now() - startedAt,
statusCode: response.status,
requestHeaders,
responseHeaders: response.headers, // fetch `Headers` and `Map` are supported
responseBody: body,
});
return body;
}
}capture() honours the configured captureRequestHeaders / captureRequestBody / captureResponseHeaders / captureResponseBody flags and the maskHeaders list, normalising any header bag (a fetch Headers, an axios AxiosHeaders, a Map, or a plain record) and redacting sensitive values with [REDACTED]. It is a no-op outside a request context, so it is safe to call unconditionally. If you have already built a final HttpRequestEntry and want to bypass the options, call recorder.record(entry) instead. The example API selects its whole ArticleGateway between the axios and fetch adapters with HTTP_CLIENT=axios|fetch, so you can run the same endpoints through either instrumentation.
For a reusable adapter — one that captures every call made with a given client, like the bundled axios/fetch adapters — implement the HttpInstrumentation interface instead of recording inline, then add it to the same instrumentations list. Hook the client's own API so you get full request and response bodies safely. Here is a complete example for got, a popular client with simple beforeRequest/afterResponse hooks:
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` runs once at bootstrap. Extend got with hooks that time each call and
// push it to the panel via the shared recorder — no per-call wiring in your services.
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;
},
],
},
});
}
}Register it like any bundled adapter — HttpCollectorModule.forRoot({ instrumentations: [AxiosInstrumentation, GotInstrumentation] }). The same shape works for undici (via diagnostics_channel), superagent (via a plugin), or a bespoke NestJS HTTP service: hook the client once in install, call recorder.capture(...) on each response.
How it works
Each selected adapter installs once at bootstrap and funnels into the shared HttpProfilerRecorder:
AxiosInstrumentationenumerates the DI container withDiscoveryService, finds every axios instance (eachHttpService, plus bare axios instances), and installs request/response interceptors on each — noaxiosRefwiring, and it never imports@nestjs/axios.FetchInstrumentationpatchesglobalThis.fetchonce.
Every adapter records the start time, then on completion the end time, status code and final URL, pushing an HttpRequestEntry to the current profile. Requests made outside a request context (e.g. in onModuleInit) are silently ignored since there is no active CLS profile.