npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nestjs-query-profiler

v0.1.0

Published

Detect N+1 queries in NestJS applications with TypeORM or Prisma

Readme

nestjs-query-profiler

Detect N+1 queries in NestJS applications. Works with TypeORM and Prisma. Shows you the line in your code that caused the problem — not a wall of framework internals.

What it does

Groups queries fired within a single request by their normalized shape. When the same shape fires threshold or more times, it warns with the call stack trimmed to your code:

[QueryProfiler] (req-abc) 12 queries in 43ms
  N+1 3x SELECT * FROM "USER" WHERE ID = ? (15ms total)
    at UserService.findAll (/src/user/user.service.ts:24:18)
    at UserController.list (/src/user/user.controller.ts:12:28)

It also reports slow queries when slowQueryThresholdMs is set:

[QueryProfiler] 1 query in 820ms
  SLOW 812ms SELECT * FROM "REPORT" WHERE tenant_id = ?

Install

npm install nestjs-query-profiler

Peer dependencies — install whichever ORM you use:

npm install @nestjs/common @nestjs/core rxjs reflect-metadata
npm install typeorm         # TypeORM users
npm install @prisma/client  # Prisma users

Setup

1. Register the module

// app.module.ts
import { NestjsQueryProfilerModule } from 'nestjs-query-profiler';

@Module({
  imports: [
    NestjsQueryProfilerModule.forRoot({
      threshold: 2,
      reporter: 'console',
      enabledEnvironments: ['development', 'test'],
    }),
  ],
})
export class AppModule {}

The module is global and automatically installs the request middleware and response interceptor across all routes.

Fastify users: the auto-wired middleware uses forRoutes('*') which only works with the default Express adapter. If you use @nestjs/platform-fastify, set autoInstall: false and apply QueryProfilerMiddleware manually with forRoutes('(.*)').

2a. TypeORM adapter

Wire the logger into your DataSource. The PROFILER_OPTIONS token gives you the resolved config via NestJS DI:

import { TypeOrmModule } from '@nestjs/typeorm';
import { PROFILER_OPTIONS } from 'nestjs-query-profiler';
import { TypeOrmQueryProfilerLogger } from 'nestjs-query-profiler/adapters/typeorm';
import type { ProfilerConfig } from 'nestjs-query-profiler';

TypeOrmModule.forRootAsync({
  useFactory: (config: ProfilerConfig) => ({
    type: 'postgres',
    // ... connection options
    maxQueryExecutionTime: 1000, // required for slow query tracking (see note below)
    logger: new TypeOrmQueryProfilerLogger(config),
  }),
  inject: [PROFILER_OPTIONS],
})

Slow query tracking with TypeORM: TypeORM only provides query duration for slow queries — those exceeding its own maxQueryExecutionTime setting. Set this on your DataSource to enable duration tracking. Queries that run faster than maxQueryExecutionTime always report 0ms. N+1 detection (which counts occurrences, not duration) is unaffected.

2b. Prisma adapter

Call installPrismaAdapter when you create your PrismaClient. It returns a new extended client — use this returned value for all queries:

import { installPrismaAdapter } from 'nestjs-query-profiler/adapters/prisma';
import { PROFILER_OPTIONS } from 'nestjs-query-profiler';
import type { ProfilerConfig } from 'nestjs-query-profiler';
import { PrismaClient } from '@prisma/client';

// In a NestJS provider:
@Injectable()
export class PrismaService {
  readonly client: PrismaClient;

  constructor(@Inject(PROFILER_OPTIONS) config: ProfilerConfig) {
    this.client = installPrismaAdapter(new PrismaClient(), config) as PrismaClient;
  }
}

Requires Prisma >=4.7. Duration is measured per-operation, so slowQueryThresholdMs works without any additional setup.

Configuration

NestjsQueryProfilerModule.forRoot({
  // Warn when the same query shape fires this many times in one request.
  // Default: 2
  threshold: 2,

  // Suppress warnings for queries whose normalized key matches any entry.
  // Accepts strings (substring match) or regular expressions.
  // Default: []
  ignore: ['SELECT 1', /^SELECT.*FROM sessions/],

  // Only enable detection in these NODE_ENV values.
  // Use ['*'] to always enable regardless of environment.
  // Default: ['development', 'test']
  enabledEnvironments: ['development', 'test'],

  // Output format. 'console' prints colored output; 'json' writes
  // newline-delimited JSON to stdout (useful in CI). Pass any object
  // implementing IReporter for custom output.
  // Default: 'console'
  reporter: 'console',

  // Console log method used for output.
  // Default: 'warn'
  logLevel: 'warn',

  // Maximum number of user-code stack frames shown per violation.
  // Default: 5
  maxStackFrames: 5,

  // Report slow queries even when no N+1 is detected. Set to 0 to disable.
  // Default: 0
  slowQueryThresholdMs: 500,

  // Register the interceptor as APP_INTERCEPTOR automatically.
  // Set to false to apply QueryProfilerInterceptor manually.
  // Default: true
  autoInstall: true,
})

Async configuration

NestjsQueryProfilerModule.forRootAsync({
  // autoInstall is resolved here, before the factory runs.
  autoInstall: true,
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    threshold: config.get<number>('QUERY_THRESHOLD', 2),
    enabledEnvironments: [config.get('NODE_ENV')],
    slowQueryThresholdMs: config.get<number>('SLOW_QUERY_MS', 0),
  }),
  inject: [ConfigService],
})

Custom reporter

Implement IReporter to route violations anywhere — Datadog, Slack, a database:

import type { IReporter, RequestSummary, ProfilerConfig } from 'nestjs-query-profiler';

class DatadogReporter implements IReporter {
  report(summary: RequestSummary, _config: ProfilerConfig) {
    for (const v of summary.violations) {
      datadogMetrics.increment('db.n1.detected', {
        query: v.normalizedKey,
        occurrences: v.occurrences,
      });
    }
  }
}

NestjsQueryProfilerModule.forRoot({ reporter: new DatadogReporter() })

JSON reporter (CI)

NestjsQueryProfilerModule.forRoot({ reporter: 'json' })

Each request summary is written to stdout as a single line of JSON. Pipe it to any log aggregator or jq:

node dist/main | jq 'select(.violations | length > 0)'

Platform support

Express (default): works out of the box with no extra configuration.

Fastify: set autoInstall: false and apply the middleware manually in your root module:

import { QueryProfilerMiddleware } from 'nestjs-query-profiler';

@Module({
  imports: [
    NestjsQueryProfilerModule.forRoot({ autoInstall: false, ... }),
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(QueryProfilerMiddleware).forRoutes('(.*)');
  }
}

Examples

The example/ directory contains ready-to-run demos:

| Command | Description | |---------|-------------| | pnpm run typeorm | N+1 detection with TypeORM + SQLite (standalone, no NestJS) | | pnpm run prisma | N+1 detection with Prisma + SQLite (standalone, no NestJS) | | pnpm run nestjs | Full NestJS app with controller/service/module — middleware + interceptor auto-wired | | pnpm run features | Standalone script showcasing ignore patterns, pause/resume, custom reporter, and JSON output |

Each demo steps through multiple scenarios (N+1 bad pattern, eager-fixed pattern, slow query) with annotated console output.

How it works

  1. Middleware opens an AsyncLocalStorage context at the start of each request. All async work spawned by the handler inherits this context automatically.
  2. ORM adapters record each query into the context store. The call stack is captured synchronously at emission time — before any await — so it correctly points to the service or repository that issued the query.
  3. Normalization strips runtime values from queries to produce a stable grouping key. TypeORM SQL is normalized by regex (string literals and numeric values replaced with ?). Prisma uses the structured action on Model form (e.g., findMany on User), which is provider-agnostic.
  4. Interceptor reads the store after the handler resolves via finalize(), which fires on both success and error paths. It calls the reporter if any query shape hit the threshold or a slow query was detected.

License

MIT