NestJS Profiler
Tutorials

TypeORM query profiling

Capture every TypeORM SQL query and inspect duration and slow queries in the profiler's Database panel.

This tutorial shows how to add the TypeORM collector to profile SQL queries in a NestJS application that uses PostgreSQL via @nestjs/typeorm.

Prerequisites

  • @eleven-labs/nest-profiler installed and configured
  • @nestjs/typeorm and typeorm installed with a working DataSource

Step 1 - Install the package

pnpm add @eleven-labs/nest-profiler-typeorm@alpha

Step 2 - Register the collector

Add TypeOrmCollectorModule after TypeOrmModule in your root module:

app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConditionalModule } from '@nestjs/config';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { TypeOrmCollectorModule } from '@eleven-labs/nest-profiler-typeorm';

const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true'

@Module({
  imports: [
    TypeOrmModule.forRootAsync({ ... }),
    ConditionalModule.registerWhen(ProfilerModule.forRoot({ isGlobal: true }), isProfilerEnabled),
    ConditionalModule.registerWhen(
      TypeOrmCollectorModule.forRoot({ slowThreshold: 100 }), // queries > 100ms highlighted
      isProfilerEnabled,
    ),
  ],
})
export class AppModule {}

No other configuration is needed - the collector injects the DataSource automatically via @InjectDataSource().

Step 3 - Instrument your services with spans

Use startSpan() to add meaningful labels to the Timeline panel alongside your TypeORM calls:

import { ProfilerService } from '@eleven-labs/nest-profiler';

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product) private readonly repo: Repository<Product>,
    private readonly profiler: ProfilerService,
  ) {}

  async findAll(): Promise<Product[]> {
    const stop = this.profiler.startSpan('db.products.findAll');
    const result = await this.repo.find({ order: { createdAt: 'DESC' } });
    stop();
    return result;
  }
}

Step 4 - Test it

Start your application and make a request that triggers a database query:

curl -i http://localhost:3000/api/v1/products

Copy the X-Debug-Token from the response headers, open /_profiler/{token}, and click the Database tab.

You will see:

  • Each SQL query with its type badge (SELECT, INSERT, …)
  • Duration per query with a bar chart indicator
  • Slow queries and N+1 patterns flagged as performance tags
  • Bound parameters

The Timeline panel shows the db.products.findAll span alongside other phases.

Driving options from ConfigService

Use forRootAsync() to resolve the options from ConfigService (or any provider), gated per environment with ConditionalModule — the recommended way (for the enabled flag, see Enabling and disabling the profiler):

import { ConditionalModule, ConfigService } from '@nestjs/config';

ConditionalModule.registerWhen(
  TypeOrmCollectorModule.forRootAsync({
    inject: [ConfigService],
    useFactory: (config: ConfigService) => ({
      slowThreshold: config.get<number>('PROFILER_SLOW_QUERY_MS') ?? 50,
    }),
  }),
  isProfilerEnabled,
);

How it works

The collector patches TypeORM's createQueryRunner() to time every query and record it into the active request profile. Streaming reads (QueryBuilder.stream()) are captured too and flagged as streaming. See How it works on the package page for the full mechanism and the exact set of captured queries.

Synchronize in production

Never use synchronize: true in production. It auto-migrates the schema on startup and can cause data loss. Use TypeORM migrations instead.

Explain slow queries

Each query in the Database panel has an Explain button. Click it and the profiler runs EXPLAIN for that one query over your DataSource and renders the execution plan inline — the top plan node, a ⚠ warning when it does a full-table (sequential) scan, the scanned relations, estimated rows/cost, and the raw plan. Supported dialects: PostgreSQL, MySQL/MariaDB and SQLite.

It runs on demand only — nothing happens until you click, so the profiled request is never slowed. EXPLAIN alone does not execute the statement; the opt-in analyze variant (EXPLAIN ANALYZE) does run the query and is therefore restricted to SELECT.

app.module.ts
TypeOrmCollectorModule.forRoot({
  explain: { enabled: true }, // default; set `enabled: false` to hide the button
  // explain: { analyze: true }, // dev only — runs the query to measure real timings (SELECT only)
});

Database panel — a query's EXPLAIN plan expanded, showing the Seq Scan badge, plan type, estimated rows/cost and the raw plan

Runs against your database

The captured SQL and its parameters are replayed on the live connection when you click, so the action sits behind the profiler's security. Values that looked like credentials were redacted at capture, so a plan for such a query may differ slightly from production.

Inspect the schema

Alongside the per-request Database panel, the package ships TypeOrmSchemaCollectorModule — a global collector that adds a Schema · TypeORM panel to the profiler home page. It introspects the DataSource once at startup and lists every registered entity with its columns, relations and indexes, so you can recall an entity's shape without leaving the profiler.

Register it next to the query collector, gated the same way:

app.module.ts
import { TypeOrmSchemaCollectorModule } from '@eleven-labs/nest-profiler-typeorm';

ConditionalModule.registerWhen(TypeOrmSchemaCollectorModule.forRoot(), isProfilerEnabled),

Open /_profiler and expand the Schema · TypeORM panel:

Schema panel — TypeORM entities with their columns, types, primary keys and defaults

The panel is introspection-only (it never touches data), honours a connectionName option for named DataSources, redacts secrets embedded in column defaults, and simply does not appear when no DataSource is wired.

Powered & maintained by

On this page