Skip to content

Migrate from nestjs-pino to logixia: A NestJS Mapping Guide ​

TL;DR: To move a NestJS app from nestjs-pino to logixia, replace LoggerModule.forRoot({ pinoHttp }) with LogixiaLoggerModule.forRoot({ ... }) from logixia/nest, replace PinoLogger and @InjectPinoLogger() with LogixiaLoggerService, keep app.useLogger(...) pointing at the new service, swap the argument order of log calls (logger.info(obj, msg) becomes logger.info(msg, obj)), and move pinoHttp.redact to logixia's redact. Two things do not map one-to-one: logixia's module does not log one line per request by itself (you add the HTTP middleware), and there is no req.log. If nestjs-pino works for you, keep it; this guide is for teams who want logixia's built-in tracing, redaction and transports. I maintain logixia.

For the broader decision, see the best NestJS logger comparison. For the pino concepts underneath (levels, redact, transports), see migrating from pino to logixia.

What does a typical nestjs-pino setup look like? ​

ts
// app.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: 'info',
        redact: ['req.headers.authorization'],
        autoLogging: { ignore: (req) => req.url === '/health' },
        customProps: () => ({ service: 'orders-api' }),
      },
    }),
  ],
})
export class AppModule {}

// order.service.ts
import { Injectable } from '@nestjs/common';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';

@Injectable()
export class OrderService {
  constructor(@InjectPinoLogger(OrderService.name) private readonly logger: PinoLogger) {}

  create(orderId: string) {
    this.logger.info({ orderId }, 'order created');
  }
}

// main.ts
import { NestFactory } from '@nestjs/core';
import { Logger } from 'nestjs-pino';

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

What is the equivalent module registration in logixia? ​

ts
import { Module } from '@nestjs/common';
import { LogixiaLoggerModule } from 'logixia/nest';

@Module({
  imports: [
    LogixiaLoggerModule.forRoot({
      appName: 'orders-api',
      environment: 'production',
      levelOptions: { level: 'info' },
      traceId: true,
      redact: { paths: ['req.headers.authorization'], autoDetect: true },
      transports: { console: { format: 'json', colorize: false } },
    }),
  ],
})
export class AppModule {}

forRoot takes the same LoggerConfig as createLogger (see configuration). The module is global, provides LogixiaLoggerService, and applies a trace middleware to every route. Async configuration works with forRootAsync (useFactory, useClass or useExisting), the counterpart of nestjs-pino's forRootAsync; the NestJS guide has the details.

nestjs-pinologixia
LoggerModule.forRoot({ pinoHttp })LogixiaLoggerModule.forRoot(loggerConfig)
pinoHttp.levellevelOptions.level
pinoHttp.redactredact (redaction guide)
pinoHttp.transport (pino-pretty, files, vendors)transports (overview); default text output is readable in development
pinoHttp.genReqIdtraceId: { generator, extractor }
pinoHttp.customPropsLogixiaContext.set() mid-request, or extraFields on the HTTP middleware
pinoHttp.autoLoggingnot automatic: add createExpressMiddleware (below)

How do I replace PinoLogger and @InjectPinoLogger? ​

Inject LogixiaLoggerService by class and set a context name. The service is transient, so each consumer gets its own instance and its own context label.

ts
import { Injectable } from '@nestjs/common';
import { LogixiaLoggerService } from 'logixia/nest';

@Injectable()
export class OrderService {
  constructor(private readonly logger: LogixiaLoggerService) {
    this.logger.setContext(OrderService.name);
  }

  async create(orderId: string): Promise<void> {
    await this.logger.info('order created', { orderId });
  }
}

Differences from PinoLogger:

  • Argument order is reversed: message first, data second.
  • Methods return promises; await them in async code.
  • No setContext string argument on every call; the context lives on the injected instance.
  • @InjectLogger() is not a drop-in for @InjectPinoLogger(name). It injects the token LOGIXIA_LOGGER_SERVICE, which forRoot() does not register. If you want per-feature loggers by token, use LogixiaLoggerModule.forFeature('Orders') and @Inject('LOGIXIA_LOGGER_ORDERS'). Injecting the class is the simpler route.
  • Error logging: logger.error('failed', { err }) serializes name, message and stack; so does logger.error(err).

How does app.useLogger change? ​

It is the same pattern with a different class:

ts
import { NestFactory } from '@nestjs/core';
import { LogixiaLoggerService } from 'logixia/nest';
import { AppModule } from './app.module';

async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule, { bufferLogs: true });
  app.useLogger(app.get(LogixiaLoggerService));
  await app.listen(3000);
}

void bootstrap();

LogixiaLoggerService implements Nest's LoggerService, so Nest's startup and framework logs go through logixia. Outside dependency injection, LogixiaLoggerModule.getGlobalLogger() returns the service once the module has booted (and null before that).

How do I get one log line per request, like pino-http? ​

nestjs-pino logs a completion line per request through pino-http. logixia's module sets up trace IDs but does not log requests on its own. Add the HTTP middleware, using the underlying logger:

ts
import { NestFactory } from '@nestjs/core';
import { createExpressMiddleware } from 'logixia/middleware';
import { LogixiaLoggerService } from 'logixia/nest';
import { AppModule } from './app.module';

async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule, { bufferLogs: true });
  const logger = app.get(LogixiaLoggerService);
  app.useLogger(logger);
  app.use(
    createExpressMiddleware(logger.getLogger(), {
      skip: (req) => req.url === '/health',
      slowRequestThresholdMs: 500,
    })
  );
  await app.listen(3000);
}

void bootstrap();

It logs request started (at debug), request completed with statusCode and duration, and a slow request detected warning, with header and query-string redaction. The autoLogging.ignore option maps to skip. See HTTP request logging.

How does request context map? ​

In nestjs-pino, request context is stored with AsyncLocalStorage by the HTTP middleware, so PinoLogger calls inside a request carry the request fields. In logixia, the module's trace middleware stores a trace ID (reusing traceparent, x-request-id and similar headers, generating one otherwise) in async context, and every log line inside the request carries it, including from services you never passed a logger to. It also echoes the ID as X-Trace-Id.

What you add:

  • Extra per-request fields (user ID, tenant): call LogixiaContext.set({ userId }) inside the request after authentication. See child loggers and context.
  • There is no req.log. Use the injected service; the trace ID is picked up from async context.
  • LogixiaExceptionFilter can replace ad-hoc error logging; it returns a consistent JSON error body and logs with the trace ID, with sensitive query values redacted in logged URLs.

What about queues, cron jobs and microservices? ​

Context outside HTTP is a known sore spot in nestjs-pino (issue #803), and logixia is not automatic here either. The trace middleware is HTTP only. The Kafka and WebSocket interceptors continue an ID already present in the message but do not generate one, and they need traceId set in forRoot(). For cron and other consumers, wrap the handler in runWithTraceId() or LogixiaContext.run(). Details are in the NestJS logger comparison.

How does redaction map? ​

pinoHttp.redact uses pino's path syntax and applies to the serialized req, res and your log objects. logixia's redact runs on the payload and message of every entry before any transport sees it:

ts
import { LogixiaLoggerModule } from 'logixia/nest';

export const loggingModule = LogixiaLoggerModule.forRoot({
  appName: 'orders-api',
  redact: {
    paths: ['**.password', 'user.creditCard'],
    patterns: [/Bearer\s+\S+/gi],
    autoDetect: 'conservative',
  },
});

Notes: there is no remove: true (values are replaced, default [REDACTED]); wildcard rules use * for one segment and ** for any depth; header and query redaction for logged requests comes from the HTTP middleware's own lists. Test your rules against real payloads, and see redaction.

What do you lose by switching? ​

  • pino's speed on simple logs and its transport ecosystem; see the benchmarks.
  • A large, tested community. nestjs-pino has about 4.3M weekly downloads (2026-11-14); logixia has roughly a hundred.
  • pino-pretty options. The built-in text output has no equivalent set of knobs.
  • Automatic pino-http request logging. You add it yourself, as above.

A safe migration plan ​

  1. Register LogixiaLoggerModule.forRoot(...) next to LoggerModule; you can run both briefly.
  2. Convert one service to LogixiaLoggerService, swap argument order and compare output.
  3. Switch app.useLogger(...) and add the HTTP middleware.
  4. Update dashboards for message, string levels and ISO timestamp.
  5. Convert the rest, then remove nestjs-pino, pino-http and pino.

Key takeaways ​

  • LoggerModule.forRoot({ pinoHttp }) becomes LogixiaLoggerModule.forRoot(config); PinoLogger becomes LogixiaLoggerService.
  • Message first, data second; await the calls.
  • Add createExpressMiddleware to get per-request completion lines.
  • Non-HTTP context needs runWithTraceId() or LogixiaContext.run().

FAQ ​

What is a good nestjs-pino alternative? nest-winston, the built-in ConsoleLogger for simple apps, or logixia if you want tracing, redaction and transports built in. See the comparison.

Can I use nestjs-pino and logixia together? Yes while migrating: register both modules and convert services one at a time.

Does logixia log every request automatically in NestJS? Not by itself. Add createExpressMiddleware from logixia/middleware, as shown above.

How do I replace @InjectPinoLogger? Inject LogixiaLoggerService and call setContext(), or use forFeature('Name') with @Inject('LOGIXIA_LOGGER_NAME').

Does logixia work with the Fastify adapter? logixia has a Fastify plugin (createFastifyPlugin) and the trace middleware tolerates Fastify-style responses, but the NestJS examples here were written for the default Express adapter, so test your setup.

Released under the MIT License.