Getting started
Install and configure @eleven-labs/nest-profiler in a NestJS 11 app - set up the core module, enable collectors, and open the /_profiler UI in minutes.
Prefer your coding agent?
Installable agent skills can do this setup for you — run npx skills add eleven-labs/nest-profiler, then ask your agent to set up the profiler. This guide covers the
manual steps.
Requirements
- Node.js 22 or newer
- A NestJS 11 application
- Any package manager (the examples below use
pnpm;npmandyarnwork too)
Installation
Install the profiler as a dev dependency — it is a development tool, and this keeps it out of your production install entirely:
pnpm add -D @eleven-labs/nest-profiler nestjs-clsThe core package declares @nestjs/common, @nestjs/core, nestjs-cls, reflect-metadata, and rxjs as peer dependencies - a NestJS application already provides all of them except nestjs-cls, which is why it is installed explicitly above.
Does your production code call the profiler?
Install it as a regular dependency instead only if a service injects TracerService (custom
spans, captureError(), currentToken()), uses the @Span() decorator or calls a collector
helper, and you want to keep that code. Those imports must resolve in production, so the profiler
is installed there and switched off with a runtime
gate.
Add a dev entrypoint
Since production does not install the profiler, it must never import it. The entrypoint is the switch: production keeps running main.ts + AppModule, untouched, and a dev-only main-dev.ts boots the same app with the profiler on top.
Start with the module that bundles the profiler. Every collector you add later goes here too:
import { Module } from '@nestjs/common';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
@Module({
imports: [ProfilerModule.forRoot({ isGlobal: true, maxProfiles: 100 })],
})
export class ProfilingModule {}A dev-only root module composes it with your application:
import { Module } from '@nestjs/common';
import { AppModule } from './app.module';
import { ProfilingModule } from './profiling/profiling.module';
@Module({ imports: [AppModule, ProfilingModule] })
export class AppDevModule {}Move the bootstrap code of main.ts into a function both entrypoints share, with a hook for the logger (more hooks can follow, e.g. for the validation pipe):
import { ConsoleLogger } from '@nestjs/common';
import type { LoggerService, Type } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
export interface BootstrapOptions {
wrapLogger?: (logger: LoggerService) => LoggerService;
}
export async function bootstrap(
rootModule: Type<unknown>,
{ wrapLogger = (logger) => logger }: BootstrapOptions = {},
): Promise<void> {
const app = await NestFactory.create(rootModule, { bufferLogs: true });
app.useLogger(wrapLogger(new ConsoleLogger('App')));
await app.listen(process.env.PORT ?? 3000);
}import { AppModule } from './app.module';
import { bootstrap } from './bootstrap';
void bootstrap(AppModule);The dev entry boots AppDevModule and wraps the logger with createProfilerLogger, so that every log entry is captured in the active profile:
import { createProfilerLogger } from '@eleven-labs/nest-profiler';
import { AppDevModule } from './app.dev.module';
import { bootstrap } from './bootstrap';
void bootstrap(AppDevModule, { wrapLogger: (logger) => createProfilerLogger(logger) });Build and run
Keep the dev files out of the production build, and give the dev script a TypeScript config that includes them:
{
"extends": "./tsconfig.json",
"exclude": [
"node_modules",
"test",
"dist",
"**/*spec.ts",
"src/main-dev.ts",
"src/app.dev.module.ts",
"src/profiling/**",
],
}{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
}{
"scripts": {
"build": "nest build",
"start": "node dist/main",
"start:dev": "nest start --watch --entryFile main-dev -p tsconfig.dev.json",
},
}pnpm start:dev now serves the profiler at /_profiler, while pnpm build && pnpm start runs the application without it. Deploy as usual: the build runs with the dev dependencies installed, and the production install (pnpm install --prod, npm ci --omit=dev, yarn install --production) leaves the profiler out.
No production file may import a profiler package at runtime (import type is fine). The install guide covers the lint rule and CI smoke test that enforce it, loggers injected through DI, and the CLI entrypoint.
Async configuration
Use forRootAsync when your options depend on injected providers such as ConfigService:
ProfilerModule.forRootAsync({
isGlobal: true,
useFactory: (config: ConfigService) => ({
storageType: config.get('PROFILER_STORAGE_TYPE', 'memory'),
}),
inject: [ConfigService],
}),Optional collector packages
Each collector is a separate package, installed as a dev dependency next to the core and registered in ProfilingModule. A collector finds what it instruments across the whole application — the DataSource, every HttpService, the global cache manager… — so nothing changes in your feature modules.
| Package | Panel | Prerequisite |
|---|---|---|
@eleven-labs/nest-profiler-typeorm | Database | TypeOrmModule.forRoot() |
@eleven-labs/nest-profiler-mikro-orm | Database | MikroOrmModule.forRoot() |
@eleven-labs/nest-profiler-mongoose | MongoDB | MongooseModule.forRoot() connection |
@eleven-labs/nest-profiler-http | HTTP Client | @nestjs/axios + axios (optional, for the bundled axios adapter) |
@eleven-labs/nest-profiler-cache | Cache | CacheModule.register({ isGlobal: true }) |
@eleven-labs/nest-profiler-auth | Security | Guard or middleware that sets request.user |
@eleven-labs/nest-profiler-config | Config | ConfigModule.forRoot({ load: [...] }) with registerAs factories |
@eleven-labs/nest-profiler-validator | Validator | A validator: class-validator (default) or another (e.g. nestjs-zod) |
@eleven-labs/nest-profiler-graphql | GraphQL | @nestjs/graphql + a driver + context factory configured |
@eleven-labs/nest-profiler-commander | Command | nest-commander + a CLI bootstrap (CommandFactory) |
@eleven-labs/nest-profiler-rabbitmq | RabbitMQ | @golevelup/nestjs-rabbitmq + amqplib |
@eleven-labs/nest-profiler-event-emitter | Events | @nestjs/event-emitter |
@eleven-labs/nest-profiler-routes | Discover | None — lists the routing table of every transport it finds |
pnpm add -D @eleven-labs/nest-profiler-typeorm @eleven-labs/nest-profiler-http @eleven-labs/nest-profiler-config @eleven-labs/nest-profiler-validatorimport { Module } from '@nestjs/common';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { TypeOrmCollectorModule } from '@eleven-labs/nest-profiler-typeorm';
import { HttpCollectorModule } from '@eleven-labs/nest-profiler-http';
import { AxiosInstrumentation } from '@eleven-labs/nest-profiler-http/axios';
import { ConfigCollectorModule } from '@eleven-labs/nest-profiler-config';
import { ValidatorCollectorModule } from '@eleven-labs/nest-profiler-validator';
@Module({
imports: [
ProfilerModule.forRoot({ isGlobal: true }),
TypeOrmCollectorModule.forRoot({ slowThreshold: 50 }),
// Nothing is instrumented unless you list it. The axios adapter auto-discovers every
// HttpService through DiscoveryService, wherever HttpModule is imported.
HttpCollectorModule.forRoot({ instrumentations: [AxiosInstrumentation] }),
ConfigCollectorModule.forRoot({ maskKeys: ['database.password'] }),
ValidatorCollectorModule.forRoot(),
],
})
export class ProfilingModule {}Calling fetch instead of (or alongside) axios? Add FetchInstrumentation from the /fetch subpath to the same list, and a phases provider from /phases to break each call into DNS, handshake, time-to-first-byte and download. See HTTP client profiling.
The Validator panel needs the profiler's validation pipe, which your bootstrap installs. Add a validationPipe hook to the shared bootstrap so production keeps a plain ValidationPipe while the dev entry swaps in the instrumented one with the same options:
import { ValidationPipe } from '@nestjs/common';
import type { PipeTransform, ValidationPipeOptions } from '@nestjs/common';
export interface BootstrapOptions {
wrapLogger?: (logger: LoggerService) => LoggerService;
validationPipe?: (options: ValidationPipeOptions) => PipeTransform;
}
// in bootstrap(), with `validationPipe = (options) => new ValidationPipe(options)` as the default:
app.useGlobalPipes(validationPipe({ whitelist: true, transform: true }));import {
createClassValidatorPipe,
createProfilerValidationPipe,
} from '@eleven-labs/nest-profiler-validator';
void bootstrap(AppDevModule, {
wrapLogger: (logger) => createProfilerLogger(logger),
validationPipe: (options) => createProfilerValidationPipe(createClassValidatorPipe(options)),
});GraphQL support
GraphQL profiling is opt-in via the dedicated @eleven-labs/nest-profiler-graphql package. The core ProfilerModule is HTTP-only - without this package, GraphQL requests pass through without profiling and without errors.
pnpm add -D @eleven-labs/nest-profiler-graphqlAdd GraphQLCollectorModule to ProfilingModule, then configure the context factory of your application's GraphQLModule so the profiler can access the underlying HTTP request. The context is plain application config, with no profiler import, so it stays in your production module:
import { GraphQLCollectorModule } from '@eleven-labs/nest-profiler-graphql';
@Module({
imports: [ProfilerModule.forRoot({ isGlobal: true }), GraphQLCollectorModule.forRoot()],
})
export class ProfilingModule {}GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
context: ({ req }) => ({ req }), // required - exposes the request to the profiler
}),GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
autoSchemaFile: true,
context: ({ request }) => ({ request }), // Mercurius uses `request` instead of `req`
});Without the context factory, the profiler silently falls back to passthrough for GraphQL resolvers. Subscriptions (WebSocket transport) are not profiled at this time.
Each captured GraphQL request appears in /_profiler with a GQL badge and the operation type. The Request tab shows the operation type, operation name, field name, syntax-highlighted query, and variables.
Custom protocol adapters
ProfilerModule exports an IContextAdapter interface and ProfilerCoreService.registerContextAdapter() that let you profile any NestJS execution context beyond HTTP - gRPC, Kafka, WebSockets, and more. @eleven-labs/nest-profiler-graphql is the reference implementation. See the Custom protocol adapters page for a full adapter example.
Storage backends
// Default - in-memory LRU, cleared on restart
ProfilerModule.forRoot({ storageType: 'memory', maxProfiles: 100 });
// File - persists to .profiler/ as JSON files, survives restarts
ProfilerModule.forRoot({ storageType: 'file', storagePath: '.profiler' });Add .profiler/ to .gitignore when using file storage.
For a persistent store that filters and paginates in the database — useful once you keep many profiles — a first-party SQLite adapter ships under the @eleven-labs/nest-profiler/sqlite subpath (opt-in: @libsql/client is an optional peer dependency, installed like the profiler with pnpm add -D @libsql/client). One adapter targets a local file, :memory:, or a remote SQLite database. Pass it through the storage option:
import { SqliteStorageAdapter } from '@eleven-labs/nest-profiler/sqlite';
// Local file
ProfilerModule.forRoot({
storage: new SqliteStorageAdapter({ path: '.profiler/profiler.db', maxProfiles: 500 }),
});
// Remote SQLite database
ProfilerModule.forRoot({
storage: new SqliteStorageAdapter({
url: process.env.PROFILER_STORAGE_URL!,
authToken: process.env.PROFILER_STORAGE_AUTH_TOKEN,
maxProfiles: 500,
}),
});See Storage backends for the full comparison of memory, file and SQLite.
Test with the example application
pnpm example:devA handful of requests is enough to light up most panels:
curl http://localhost:3000/api/v1/products # Catalog (SQL — mikro-orm by default, or SQL_ORM=typeorm)
curl http://localhost:3000/api/v1/articles # Outgoing HTTP + cache SET
curl http://localhost:3000/api/v1/articles # cache HIT
curl http://localhost:3000/api/v1/slow # Nested trace spans
curl -X POST http://localhost:3000/api/v1/products \
-H "Content-Type: application/json" \
-d '{"name":"Hi","price":-1}' # Validator violations
# GraphQL - requires FEATURE_GRAPHQL=true (default), served over the catalog
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{
"operationName": "GetProduct",
"query": "query GetProduct($id: Int!) { product(id: $id) { id name price } }",
"variables": { "id": 1 }
}'
# Field resolvers - one operation, three collectors (needs FEATURE_MONGOOSE=true):
# SQL products, then their MongoDB reviews, then each review's author over HTTP
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{
"operationName": "Products",
"query": "query Products { products { id name reviews { rating author { name company } } } }"
}'The full endpoint catalogue — one table per bounded context, with the panel each call fills — lives on the Example application page.
Open http://localhost:3000/_profiler to browse all collected profiles.
Something not showing up? The Troubleshooting page covers the common cases - no profiles, an empty Logs tab, GraphQL not captured, a missing collector panel, or a 401 on the UI.
Introduction
An open-source NestJS profiler inspired by Symfony's Web Profiler - inspect SQL, HTTP, GraphQL, cache, auth and custom spans to debug and optimize performance.
Troubleshooting
Common issues when profiling a NestJS app - no profiles appear, empty Logs tab, GraphQL not captured, missing collector panels, 401 on the UI - and how to fix each one.