NestJS Profiler
Tutorials

Command profiling

Profile nest-commander CLI commands and inspect them in the web profiler - the console equivalent of Symfony's command profiling.

This tutorial shows how to profile CLI commands built with nest-commander. Each command run produces a profile - with a Command panel plus any HTTP, cache, or database activity the command triggered - that appears alongside your HTTP profiles at /_profiler.

Prerequisites

  • @eleven-labs/nest-profiler installed and configured with file storage
  • nest-commander installed

Command profiling needs a cross-process storage adapter (storageType: 'file', or a Redis/DB adapter). The CLI and the web server are separate processes, so in-memory storage - local to a single process - can never surface command profiles in the server. The profiler logs a warning if you profile a command against an in-memory store.

Step 1 - Install the packages

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

Step 2 - Write a command

You do not change anything for profiling - write an ordinary nest-commander command:

sync-posts.command.ts
import { Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { HttpService } from '@nestjs/axios';
import type { Cache } from 'cache-manager';
import { firstValueFrom } from 'rxjs';
import { Command, CommandRunner, Option } from 'nest-commander';

@Command({ name: 'sync:posts', description: 'Fetch posts and cache them' })
export class SyncPostsCommand extends CommandRunner {
  constructor(
    private readonly http: HttpService,
    @Inject(CACHE_MANAGER) private readonly cache: Cache,
  ) {
    super();
  }

  async run(_params: string[], options?: { limit?: number }): Promise<void> {
    const { data } = await firstValueFrom(
      this.http.get(`https://jsonplaceholder.typicode.com/posts?_limit=${options?.limit ?? 5}`),
    );
    await this.cache.set('cli:posts', data, 60000);
  }

  @Option({ flags: '-l, --limit <limit>', description: 'Number of posts' })
  parseLimit(value: string): number {
    return parseInt(value, 10);
  }
}

Step 3 - Register the CLI module

Create a dedicated module for the CLI. Use file storage with the same storagePath as your HTTP app so the two processes share profiles:

cli.module.ts
import { Module } from '@nestjs/common';
import { ConditionalModule, ConfigModule } from '@nestjs/config';
import { CacheModule } from '@nestjs/cache-manager';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { CommanderCollectorModule } from '@eleven-labs/nest-profiler-commander';
import { HttpModule } from '@nestjs/axios';
import { HttpCollectorModule } from '@eleven-labs/nest-profiler-http';
import { CacheCollectorModule } from '@eleven-labs/nest-profiler-cache';
import { SyncPostsCommand } from './sync-posts.command';

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

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ConditionalModule.registerWhen(
      ProfilerModule.forRoot({ isGlobal: true, storageType: 'file', storagePath: '.profiler' }),
      isProfilerEnabled,
    ),
    ConditionalModule.registerWhen(CommanderCollectorModule.forRoot(), isProfilerEnabled),
    HttpModule, // provides HttpService for the bundled axios adapter
    ConditionalModule.registerWhen(HttpCollectorModule.forRoot(), isProfilerEnabled),
    CacheModule.register({ isGlobal: true }),
    ConditionalModule.registerWhen(CacheCollectorModule.forRoot(), isProfilerEnabled),
  ],
  providers: [SyncPostsCommand],
})
export class CliModule {}

Your CLI root module must import ConfigModule.forRoot() from @nestjs/config. The recommended gating (ConditionalModule.registerWhen) awaits ConfigModule.envVariablesLoaded, which only resolves once ConfigModule.forRoot() has run. A CommandFactory CLI that never imports it never resolves that promise, so registration hangs and the process exits 0 silently - no logs, no error (the internal timeout is unref'd). HTTP apps rarely hit this because their root module already imports ConfigModule.forRoot(). This applies to every module the CLI loads that gates something with registerWhen (core, database, RabbitMQ collectors…).

Step 4 - Bootstrap the CLI

cli.ts
import 'reflect-metadata';
import { CommandFactory } from 'nest-commander';
import { CliModule } from './cli.module';

async function bootstrap(): Promise<void> {
  await CommandFactory.run(CliModule, { logger: ['error', 'warn'] });
}

void bootstrap();

Bootstrap the CLI with a compiler that emits decorator metadata (tsc / nest start), not a plain esbuild/tsx runner - otherwise NestJS cannot resolve your command's injected dependencies.

Step 5 - Run a command

# build first, then run the compiled entry
node dist/cli.js sync:posts --limit 3

Step 6 - Inspect the profile

Start your HTTP app (pointed at the same storagePath) and open /_profiler. Pick Commands in the sidebar: commands get their own view, with a Status filter for successful vs failed runs. Open one to find:

  • Command - name, arguments, options, duration, exit code (the request/response tabs are hidden)
  • HTTP Client - the outgoing request the command made
  • Cache - the SET operation

A command that throws is captured too: its profile is marked failed (status 500) and the thrown error appears in the Exceptions tab.

How it works

At bootstrap the module wraps every CommandRunner.run() to synthesise a command profile, and registers the command entrypoint type with the profiler core (the Commands list view and Command detail tab) - so import it in the HTTP app too when you want command profiles to render there. See How it works on the package page.

Powered & maintained by

On this page