Skip to content
NestJS ns config 4 min read

Logging

NestJS ships with a built-in text-based logger that works out of the box during bootstrap and at runtime. It is designed to be flexible: you can tune which severity levels appear, attach a context string to every message, buffer early logs until your custom logger is ready, or swap the implementation entirely for something like Pino or Winston. Understanding the built-in Logger and the LoggerService contract is the foundation for every logging decision you make later.

Using the built-in Logger

The simplest way to log is to instantiate the Logger class and give it a context string. The context appears in brackets in every line, which makes it easy to grep production output for a specific provider or controller.

import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class PaymentsService {
  private readonly logger = new Logger(PaymentsService.name);

  charge(orderId: string, amount: number): void {
    this.logger.log(`Charging order ${orderId} for ${amount} cents`);

    try {
      // perform the charge against the payment gateway
      this.logger.debug(`Gateway accepted order ${orderId}`);
    } catch (err) {
      this.logger.error(`Charge failed for ${orderId}`, (err as Error).stack);
    }
  }
}

Passing PaymentsService.name means the class name is the context, so you never hard-code a string that can drift from the file it lives in.

Output:

[Nest] 18420  - 06/14/2026, 9:41:02 AM     LOG [PaymentsService] Charging order ord_91 for 4999 cents
[Nest] 18420  - 06/14/2026, 9:41:02 AM   DEBUG [PaymentsService] Gateway accepted order ord_91

The Logger exposes one method per severity level: log, error, warn, debug, verbose, and fatal. The error method accepts an optional second argument for a stack trace.

Configuring log levels

By default, every level except debug and verbose is enabled in production-style builds. You control the active levels when you create the application by passing the logger option. Levels are cumulative in importance but you list them explicitly.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log'],
  });
  await app.listen(3000);
}
bootstrap();

You can also disable logging entirely by passing logger: false, which is the right choice for most test runs.

LevelUse it forOn by default
fatalUnrecoverable errors before a crashYes
errorHandled errors and failed operationsYes
warnSuspicious but non-fatal conditionsYes
logNormal application lifecycle eventsYes
debugDetailed flow information for developmentNo
verboseExtremely fine-grained tracingNo

Tip: Drive the level array from an environment variable rather than hard-coding it, so you can enable debug in staging without a redeploy. Read more in Environment variables.

Buffering logs during bootstrap

If you provide a custom logger that itself relies on dependency injection, it is not available until the application has been fully initialized. Any messages emitted before that point would normally be lost or printed with the default logger. The bufferLogs option holds those early messages in memory and flushes them once your custom logger is attached.

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});

app.useLogger(app.get(MyLogger));
await app.listen(3000);

With bufferLogs: true, even framework messages produced during module resolution are replayed through MyLogger after the call to useLogger, so you get a single consistent format from the very first line.

Injecting a custom logger

To replace the default implementation, write a class that satisfies the LoggerService interface. Because it is a normal provider, it can inject configuration, request transports, or anything else from the DI container.

import { Injectable, LoggerService, ConsoleLogger } from '@nestjs/common';

@Injectable()
export class JsonLogger extends ConsoleLogger implements LoggerService {
  log(message: unknown, context?: string) {
    process.stdout.write(
      JSON.stringify({ level: 'log', context, message }) + '\n',
    );
  }

  error(message: unknown, stack?: string, context?: string) {
    process.stderr.write(
      JSON.stringify({ level: 'error', context, message, stack }) + '\n',
    );
  }
}

Register it as a provider, mark it global so any module can inject it, and wire it into the application:

import { Module } from '@nestjs/common';
import { JsonLogger } from './json-logger';

@Module({
  providers: [JsonLogger],
  exports: [JsonLogger],
})
export class LoggingModule {}
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(JsonLogger));

Extending ConsoleLogger lets you override only the methods you care about while inheriting the rest, including level filtering and timestamp diffing.

Disabling logging in tests

Noisy logs make test output hard to read and can slow CI. In unit tests built with Test.createTestingModule, override the logger after the app is created.

import { Test } from '@nestjs/testing';

const moduleRef = await Test.createTestingModule({
  imports: [AppModule],
}).compile();

const app = moduleRef.createNestApplication();
app.useLogger(false);
await app.init();

Passing false to useLogger silences all output for the lifetime of that application instance, leaving your assertions free of framework chatter.

Best Practices

  • Always pass a context string (use ClassName.name) so logs are filterable and never go stale.
  • Choose active levels from configuration, not literals, so you can raise verbosity without a code change.
  • Reserve error and fatal for genuine failures; overusing them defeats alerting.
  • Turn on bufferLogs whenever your logger depends on DI, so no early message is dropped or misformatted.
  • Extend ConsoleLogger instead of implementing LoggerService from scratch to keep level filtering for free.
  • Disable logging in tests with useLogger(false) to keep CI output clean and fast.
  • Emit structured JSON in production so logs are machine-parseable by your aggregation stack.
Last updated June 14, 2026
Was this helpful?