NestJS Profiler
Tutorials

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-profiler installed and configured
  • For the axios adapter only: @nestjs/axios and axios installed (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 axios

Step 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.

posts.module.ts
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

posts.service.ts
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/posts

Copy 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/posts

Wire 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.

weather.service.ts
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:

got.instrumentation.ts
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:

  • AxiosInstrumentation enumerates the DI container with DiscoveryService, finds every axios instance (each HttpService, plus bare axios instances), and installs request/response interceptors on each — no axiosRef wiring, and it never imports @nestjs/axios.
  • FetchInstrumentation patches globalThis.fetch once.

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.

Powered & maintained by

On this page