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.
| Level | Use it for | On by default |
|---|---|---|
fatal | Unrecoverable errors before a crash | Yes |
error | Handled errors and failed operations | Yes |
warn | Suspicious but non-fatal conditions | Yes |
log | Normal application lifecycle events | Yes |
debug | Detailed flow information for development | No |
verbose | Extremely fine-grained tracing | No |
Tip: Drive the level array from an environment variable rather than hard-coding it, so you can enable
debugin 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
errorandfatalfor genuine failures; overusing them defeats alerting. - Turn on
bufferLogswhenever your logger depends on DI, so no early message is dropped or misformatted. - Extend
ConsoleLoggerinstead of implementingLoggerServicefrom 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.