How to migrate from @nestjs/microservices NATS to JetStream
Use whenyou are replacing Transport.NATS from /microservices.
You getfive steps that keep your handlers, plus the acknowledgment semantics that change underneath.
Replacing the built-in NestJS NATS transport (Transport.NATS from @nestjs/microservices) with @horizon-republic/nestjs-jetstream is configuration work: your ClientProxy already speaks NATS.
You will end up with durable delivery and automatic retries, plus dead letter handling and W3C trace context, all behind the same @EventPattern and @MessagePattern handlers you already have.
Upgrading lists what changes between its versions.
What changes
The semantic shift is from at-most-once to at-least-once delivery:
- Delivery. Built-in NATS is fire-and-forget, so a message with no subscriber listening is gone. JetStream persists every event in a stream, and messages survive restarts.
- Retention. Built-in has no retention. JetStream keeps messages in a stream until acked (workqueue) or until the retention window expires.
- Replay. Built-in does not support replay. JetStream consumers can be created to catch up on history.
- Retries. Built-in does not retry. JetStream redelivers a message until the handler acks or
max_deliveris exhausted. - Dead letters. Built-in silently drops failed messages. JetStream provides a configurable dead-letter flow (Dead Letter Queue).
- Decorators. Unchanged.
@EventPatternand@MessagePatternwork identically.
Step 1: Install the library
- npm
- pnpm
- Yarn
npm install @horizon-republic/nestjs-jetstream
pnpm add @horizon-republic/nestjs-jetstream
yarn add @horizon-republic/nestjs-jetstream
The NATS client packages come with it. Keep @nestjs/microservices: it is a peer dependency, and the decorators and ClientProxy still come from it.
Step 2: Replace module registration
Before:
import { Transport, ClientsModule } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{
name: 'NATS_SERVICE',
transport: Transport.NATS,
options: { servers: ['nats://localhost:4222'] },
},
]),
],
})
export class AppModule {}
After:
import { JetstreamModule } from '@horizon-republic/nestjs-jetstream';
@Module({
imports: [
// Once in your root module, creates the connection + consumer infrastructure
JetstreamModule.forRoot({
name: 'orders',
servers: ['nats://localhost:4222'],
}),
// Per target service, where you publish to
JetstreamModule.forFeature({ name: 'payments' }),
],
})
export class AppModule {}
The name field is the service identity used to derive stream, consumer, and subject names. See Naming Conventions for the rules.
Step 3: Keep your handlers
No changes to handler signatures. @EventPattern and @MessagePattern work identically.
import { Controller } from '@nestjs/common';
import { EventPattern, MessagePattern, Payload } from '@nestjs/microservices';
@Controller()
export class OrdersController {
@EventPattern('order.created')
handleOrderCreated(@Payload() data: { orderId: string }) {
// Same code as before. Throws here will trigger JetStream retries.
}
@MessagePattern('order.get')
getOrder(@Payload() data: { id: string }) {
return { id: data.id, status: 'shipped' };
}
}
Step 4: Replace client injection
Before:
import { ClientProxy } from '@nestjs/microservices';
constructor(@Inject('NATS_SERVICE') private readonly client: ClientProxy) {}
After:
import { ClientProxy } from '@nestjs/microservices';
import { getClientToken } from '@horizon-republic/nestjs-jetstream';
constructor(@Inject(getClientToken('payments')) private readonly client: ClientProxy) {}
client.emit() and client.send() keep their existing signatures.
Step 5: Adjust for acknowledgment semantics
JetStream is at-least-once. A message may be redelivered after a handler throws or a pod restarts mid-execution. Make handlers idempotent:
- Use a unique identifier from the payload (e.g.,
orderId) to deduplicate. - For commands that produce side effects (charge a card, send an email), check whether the side effect already happened before doing it again.
- See Idempotency in the events pattern for concrete techniques.
What you gain
Messages survive restarts and are retried up to max_deliver, then kept in a dead-letter stream. Health checks, graceful shutdown, tracing and metrics are available behind the same module. Why JetStream? has the full comparison.
See also
- Installation: setup requirements
- Module Configuration: full options reference
- Quick Start: first handler in 5 minutes
- Upgrading: changes between versions of this library