Skip to main content

Durable messaging for NestJS microservices

A NestJS transport for NATS JetStream. Keep @EventPattern, @MessagePattern and ClientProxy as they are: messages persist across deploys, failed handlers retry, and what still fails is kept for you to inspect.

Built for NestJSRuns on NATS JetStream

  • v3.0.0
  • NestJS 10, 11, 12
  • NATS ≥ 2.10
  • Node ≥ 22
  • MIT

Nothing lost on deploy

A message stays in a JetStream stream until a handler acks it. If the pod restarts mid-handler, another replica picks the message up.

Failures have somewhere to go

A throw retries after 2 s, then 10 s. A message that still fails is kept in a dead-letter stream with its headers, never dropped.

The NestJS API you already use

Handlers, ClientProxy and exception filters stay as they are. What changes is the module you import.

When the built-in transport is not enough

NestJS ships a NATS transport on Core NATS: fast, and fire and forget. It suits cache invalidations and other messages you can afford to lose. For work that must happen, this library adds what Core NATS leaves out.

Read the full comparison
CapabilityTransport.NATSnestjs-jetstream
@EventPattern, @MessagePattern and ClientProxy✓✓
Low-latency request and reply✓✓
Messages survive a restart–✓
Retries when a handler throws–✓
Dead-letter stream–✓
Broadcast with catch-up for late replicas–✓
Replay of past events–✓
Cron and delayed delivery–✓
OpenTelemetry tracing and Prometheus metrics–✓

Switching is a module swap

  1. Install the package. The NATS client comes with it.
  2. Swap the module. forRoot for the service, forFeature for each target it calls.
  3. Connect the transport. One connectMicroservice call in main.ts.
  4. Keep your handlers. Make them idempotent: delivery is now at least once.
Migration guide
app.module.ts+2 -1
ClientsModule.register([{ name: 'ORDERS', transport: Transport.NATS }]),
JetstreamModule.forRoot({ name: 'orders', servers }),
JetstreamModule.forFeature({ name: 'orders' }),

Run your first durable handler

The quick start takes about five minutes against a local NATS in Docker.