Tous les articles
NestJSTypeScriptBackend

NestJS 11: JSON Logging, Microservice Introspection, and Express v5 Route Syntax

//
·7 min de lecture

NestJS 11 ships structured JSON logging, microservice status observables and unwrap(), a new ParseDatePipe, Express v5 wildcard route syntax changes, and improved dynamic module singleton behaviour.

NestJS 11 released on January 22, 2025 and has been steadily receiving patches — the latest is 11.1.17. The release modernises the logger, gives microservice clients observable status streams, and introduces breaking changes in Express v5 route syntax and dynamic module singleton behaviour that require attention before upgrading.

>Node.js compatibility

⚠ BREAKINGNode.js 16 is dropped. Minimum is Node 20 LTS. Ensure your CI pipeline, Dockerfile and hosting environment are all on 20+.

>Structured JSON logging

ConsoleLogger now supports structured JSON output — ideal for log aggregation pipelines (Datadog, Loki, CloudWatch).

TypeScript
// main.ts
import { NestFactory }     from '@nestjs/core';
import { ConsoleLogger }   from '@nestjs/common';
import { AppModule }       from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: new ConsoleLogger({
      json:   true,    // structured JSON output for production
      colors: false,   // disable ANSI in JSON mode
    }),
  });
  await app.listen(3000);
}

// Output per log line:
// { "level": "log", "message": "...", "context": "...", "timestamp": "..." }

// Development: pretty colors
const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({ json: false, colors: true }),
});

>Microservice introspection — unwrap, on(), status$

Client proxies now expose the underlying transport connection, event listeners and an observable status stream.

TypeScript
import { ClientProxy }   from '@nestjs/microservices';
import { NatsConnection } from 'nats';

@Injectable()
export class OrdersService {
  constructor(
    @Inject('NOTIFICATIONS_SERVICE')
    private client: ClientProxy,
  ) {}

  onModuleInit() {
    // 1. unwrap() — access the raw transport connection
    const nats = this.client.unwrap<NatsConnection>();
    console.log('NATS server info:', nats.info);

    // 2. on() — listen to transport events
    this.client.on<NatsEvents>('disconnect', () => {
      console.warn('NATS client disconnected');
    });

    // 3. status$ — reactive status stream (RxJS Observable)
    this.client.status.subscribe((status) => {
      console.log('Client status:', status); // 'connected' | 'disconnected' | ...
    });
  }
}

>ParseDatePipe — new built-in pipe

TypeScript
import { Controller, Get, Query, ParseDatePipe } from '@nestjs/common';

@Controller('events')
export class EventsController {

  @Get()
  findBetween(
    @Query('from', ParseDatePipe) from: Date,
    @Query('to',   new ParseDatePipe({ optional: true })) to?: Date,
  ) {
    // from and to are Date objects — ParseDatePipe handles ISO 8601 strings
    return this.eventsService.findBetween(from, to);
  }
}

>Breaking: Express v5 wildcard route syntax

⚠ BREAKINGExpress v5 changed wildcard and optional route syntax. Any @Get('path/*') pattern must be updated.

TypeScript
// ✗ Express v4 syntax — breaks silently in v5
@Get('files/*')
@Get('assets/:file.:ext?')
@Get('docs/v:version?')

// ✓ Express v5 syntax
@Get('files/{*splat}')              // named wildcard required
@Get('assets/:file{.:ext}')         // optional segment with braces
@Get('docs/v:version?')             // optional param — unchanged

// Complete example
@Controller('storage')
export class StorageController {

  @Get('{*path}')                   // matches /storage/a/b/c
  serveFile(@Param('path') path: string) {
    return this.storageService.read(path);
  }
}

>Breaking: dynamic module singleton behaviour

⚠ BREAKINGIn NestJS 11, two dynamic module calls with deeply equal config objects now create separate module instances instead of sharing one. Wrap the module call in a variable to preserve singleton behaviour.

TypeScript
// ✗ Before — NestJS 10 treated these as the same instance
@Module({
  imports: [
    ConfigModule.forFeature(dbConfig),  // instance A
    ConfigModule.forFeature(dbConfig),  // was A, now B — new behaviour!
  ],
})

// ✓ Correct — reference the same object
const DbConfigModule = ConfigModule.forFeature(dbConfig);

@Module({
  imports: [
    DbConfigModule,  // instance A
    DbConfigModule,  // still A — singleton preserved
  ],
})

>Breaking: lifecycle hook order reversed on shutdown

⚠ BREAKINGTermination lifecycle hooks (OnModuleDestroy, OnApplicationShutdown) now fire in reverse registration order. Review shutdown logic in apps with ordered teardown requirements.

>IntrospectionException — bypass auto-logging

TypeScript
import { IntrospectionException } from '@nestjs/core';

@Injectable()
export class HealthService {
  check(): void {
    // Throws without logging to console — useful for expected error states
    // that you handle upstream (e.g. health-check returning 503)
    throw new IntrospectionException('Service unavailable');
  }
}

>Upgrade checklist

  • Bump Node.js to 20+ everywhere (CI, Dockerfile, hosting).
  • Update all @nestjs/* packages to ^11.0.0 together.
  • Upgrade express to ^5 or fastify to ^5.
  • Audit all wildcard and optional route decorators for Express v5 syntax.
  • Search for duplicate dynamic module calls — wrap shared configs in a variable.
  • Review shutdown hooks if your app relies on teardown order.
  • Replace ConsoleLogger options if you had a custom logger setup.