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
pnpm add @eleven-labs/nest-profiler@alpha nestjs-clsAlpha release
There is no stable release yet — every @eleven-labs/nest-profiler* package must be installed
with the @alpha dist-tag. @latest resolves to nothing.
The 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.
Want it in devDependencies only?
If you never want the profiler in production — zero footprint, never installed there — install it
with pnpm add -D @eleven-labs/nest-profiler@alpha nestjs-cls and use the dev-entry
split instead
of the runtime gate below.
Configure the core module
The profiler is a development tool — keep it off in production. The recommended way to wire it is to gate the active ProfilerModule with ConditionalModule.registerWhen. When profiling is off, the active module is never loaded, at no runtime cost.
import { Module } from '@nestjs/common';
import { ConditionalModule } from '@nestjs/config';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';
@Module({
imports: [
ConditionalModule.registerWhen(
ProfilerModule.forRoot({ isGlobal: true, maxProfiles: 100 }),
isProfilerEnabled,
),
],
})
export class AppModule {}Log capture keeps working when off with nothing extra (createProfilerLogger is DI-free — see below). Only if a service injects ProfilerService directly (custom startSpan, events, exceptions…) do you also register ProfilerNoopModule.forRoot({ isGlobal: true }), gated on (env) => !isProfilerEnabled(env), so that injection still resolves — see Enabling and disabling.
Async configuration
Use forRootAsync when your options depend on injected providers such as ConfigService — gate it the same way:
ConditionalModule.registerWhen(
ProfilerModule.forRootAsync({
isGlobal: true,
useFactory: (config: ConfigService) => ({
storageType: config.get('PROFILER_STORAGE_TYPE', 'memory'),
}),
inject: [ConfigService],
}),
isProfilerEnabled,
),Turn the profiler on and off
The wiring shown above is the recommended toggle: the isProfilerEnabled predicate (here reading PROFILER_ENABLED) decides whether the active ProfilerModule is loaded, so the profiler is off in production.
Gate each optional collector package the same way. When several profiler modules sit at the root, group them into a single module (the example app bundles the core plus its global collectors into one ProfilingModule) so the composition root keeps a single gate for the active bundle — plus the no-op fallback only if the app injects ProfilerService directly. A top-level enabled option is also supported as an alternative, documented once in Configuration.
Enable log capture
Wrap the NestJS logger in main.ts so that every log entry is captured in the active request profile:
import { ConsoleLogger } from '@nestjs/common';
import { createProfilerLogger } from '@eleven-labs/nest-profiler';
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(createProfilerLogger(new ConsoleLogger('MyApp')));
await app.listen(3000);Optional collector packages
Each collector is a separate package installed alongside the core. Import each one in the feature module it instruments - not in the root module - so the dependency is co-located with the code it observes.
| 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) |
Module-per-collector pattern
Each optional collector is registered in the feature module it instruments, gated with ConditionalModule.registerWhen using the same isProfilerEnabled predicate as above:
import { ConditionalModule } from '@nestjs/config';
import { TypeOrmCollectorModule } from '@eleven-labs/nest-profiler-typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([Product]),
ConditionalModule.registerWhen(
TypeOrmCollectorModule.forRoot({ slowThreshold: 50 }),
isProfilerEnabled,
),
],
})
export class ProductsModule {}import { ConditionalModule } from '@nestjs/config';
import { MikroOrmCollectorModule } from '@eleven-labs/nest-profiler-mikro-orm';
@Module({
imports: [
MikroOrmModule.forFeature([Product]),
ConditionalModule.registerWhen(
MikroOrmCollectorModule.forRoot({ slowThreshold: 50 }),
isProfilerEnabled,
),
],
})
export class ProductsModule {}import { ConditionalModule } from '@nestjs/config';
import { HttpModule, HttpService } from '@nestjs/axios';
import { HttpCollectorModule } from '@eleven-labs/nest-profiler-http';
import { CacheCollectorModule } from '@eleven-labs/nest-profiler-cache';
@Module({
imports: [
HttpModule, // provides HttpService
// The profiler never imports @nestjs/axios — hand it your HttpService.axiosRef:
ConditionalModule.registerWhen(
HttpCollectorModule.forRootAsync({
imports: [HttpModule],
inject: [HttpService],
useFactory: (http: HttpService) => ({ axiosRef: http.axiosRef }),
}),
isProfilerEnabled,
),
ConditionalModule.registerWhen(CacheCollectorModule.forRoot(), isProfilerEnabled),
],
})
export class ContentModule {}import { ConditionalModule } from '@nestjs/config';
import { AuthCollectorModule } from '@eleven-labs/nest-profiler-auth';
@Module({
imports: [
ConditionalModule.registerWhen(
AuthCollectorModule.forRoot({ maskUserFields: ['password'] }),
isProfilerEnabled,
),
],
})
export class AuthModule {}Collectors that read global providers or contribute a global panel should remain in the root module:
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { ConfigCollectorModule } from '@eleven-labs/nest-profiler-config';
import { ValidatorCollectorModule } from '@eleven-labs/nest-profiler-validator';
import { ConditionalModule } from '@nestjs/config';
const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [appConfig, dbConfig] }),
CacheModule.register({ isGlobal: true }),
// Profiler + root-level collectors, gated with ConditionalModule.
ConditionalModule.registerWhen(ProfilerModule.forRoot({ isGlobal: true }), isProfilerEnabled),
ConditionalModule.registerWhen(
ConfigCollectorModule.forRoot({ maskKeys: ['database.password'] }),
isProfilerEnabled,
),
// Validator panel only — the pipe is app-owned in main.ts (below), so validation runs
// regardless of the profiler's gate.
ConditionalModule.registerWhen(ValidatorCollectorModule.forRoot(), isProfilerEnabled),
// feature modules:
ProductsModule,
ContentModule,
AuthModule,
],
})
export class AppModule {}The Validator panel above needs its validation pipe installed in your bootstrap. This keeps validation independent of the profiler's gate — it runs whether profiling is on or off:
import {
createProfilerValidationPipe,
createClassValidatorPipe,
} from '@eleven-labs/nest-profiler-validator';
app.useGlobalPipes(
createProfilerValidationPipe(createClassValidatorPipe({ whitelist: true, transform: true })),
);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 @eleven-labs/nest-profiler-graphql@alphaImport GraphQLCollectorModule alongside ProfilerModule, then configure the context factory for your driver so the profiler can access the underlying HTTP request:
import { ConditionalModule } from '@nestjs/config';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { GraphQLCollectorModule } from '@eleven-labs/nest-profiler-graphql';
const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';
@Module({
imports: [
ConditionalModule.registerWhen(ProfilerModule.forRoot({ isGlobal: true }), isProfilerEnabled),
ConditionalModule.registerWhen(GraphQLCollectorModule.forRoot(), isProfilerEnabled),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
context: ({ req }) => ({ req }), // required - exposes the request to the profiler
}),
],
})
export class AppModule {}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). 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:devMake requests to exercise each collector:
curl http://localhost:3000/api/v1/products # Catalog (in-memory by default, or SQL SELECT with SQL_ORM=typeorm)
curl http://localhost:3000/api/v1/articles # Axios GET + cache SET
curl http://localhost:3000/api/v1/articles # cache HIT
curl http://localhost:3000/api/v1/auth/token # get demo JWT
curl -H "Authorization: Bearer <token>" http://localhost:3000/api/v1/auth/me # Auth
curl -X POST http://localhost:3000/api/v1/articles \
-H "Content-Type: application/json" \
-d '{"title":"Hi","body":"too short"}' # Validator violations
curl http://localhost:3000/api/v1/slow # Timeline spans
# GraphQL - requires FEATURE_GRAPHQL=true (default), served over the catalog
# Query with operation name
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{
"operationName": "GetProducts",
"query": "query GetProducts { products { id name price inStock } }"
}'
# Query with variable
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 }
}'
# Mutation with operation name and variables
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{
"operationName": "CreateProduct",
"query": "mutation CreateProduct($input: CreateProductInput!) { createProduct(input: $input) { id name price } }",
"variables": { "input": { "name": "NestJS in Action", "price": 29.99 } }
}'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.