Migration Guide
From @nestjs/microservices NATS transport
If your ClientProxy already talks to NATS, switching to JetStream is mostly configuration. Here are the five steps.
What changes
| Aspect | Built-in NATS | nestjs-jetstream |
|---|---|---|
| Delivery | At-most-once (fire-and-forget) | At-least-once (persistent) |
| Retention | None — messages lost if no subscriber | Stream-based — messages survive restarts |
| Replay | Not supported | New consumers catch up on history |
| Fan-out | All subscribers get every message | Workqueue (one handler) or Broadcast (all) |
| RPC | Core request/reply | Core (default) or JetStream-backed |
| DLQ | Not supported | Dedicated DLQ stream with tracking headers, or onDeadLetter callback |
| Ack/Nak | Not applicable | Explicit acknowledgment per message |
Step 1 — Install the library
pnpm add @horizon-republic/nestjs-jetstream
The @nats-io/* client packages come in as direct dependencies — you don't have to install them yourself. Only your NestJS peer dependencies matter: make sure your project meets the runtime requirements (Node.js, NestJS, TypeScript versions).
Step 2 — Replace module registration
Before (built-in):
// main.ts
app.connectMicroservice({
transport: Transport.NATS,
options: { servers: ['nats://localhost:4222'] },
});
After (nestjs-jetstream):
// app.module.ts
@Module({
imports: [
JetstreamModule.forRoot({
name: 'my-service',
servers: ['nats://localhost:4222'],
}),
],
})
export class AppModule {}
Then connect the transport in your bootstrap file, just like in the Quick Start:
const app = await NestFactory.create(AppModule);
app.connectMicroservice({ strategy: app.get(JetstreamStrategy) }, { inheritAppConfig: true });
await app.startAllMicroservices();
Step 3 — Keep your handlers
Your existing @EventPattern() and @MessagePattern() handlers work as-is. The decorators are the same — only the underlying transport changes.
// Works with both transports — no code changes needed
@EventPattern('user.created')
async handleUserCreated(@Payload() data: UserDto) {
await this.userService.process(data);
}
@MessagePattern('user.get')
async getUser(@Payload() id: string) {
return this.userService.findById(id);
}
Step 4 — Replace client injection
Before (built-in):
@Inject('NATS_SERVICE') private readonly client: ClientProxy
After (nestjs-jetstream):
// Register in the module
JetstreamModule.forFeature({ name: 'users' })
// Inject with the service name as the token
@Inject('users') private readonly client: ClientProxy
Step 5 — Adjust for acknowledgment semantics
The key behavioral difference: messages are now acknowledged explicitly. If your handler throws, the message is retried (up to max_deliver times, default 3).
Idempotency matters now. If a handler is called twice with the same message, the second call should produce the same result. Use message deduplication or idempotent operations.
What you gain
After migration, you get for free:
- Messages survive NATS server restarts
- Failed messages are automatically retried
- Dead letter handling for exhausted retries
- Health checks with RTT monitoring
- Graceful shutdown with message drain
- Broadcast fan-out to all service instances
- Ordered sequential delivery mode
Upgrading between versions
v2.9 → v2.10
New features:
- Distributed tracing with OpenTelemetry — every publish, consume, and RPC round-trip now produces an OpenTelemetry span. W3C Trace Context propagates through NATS message headers, so a single trace flows end-to-end across services. Activates automatically the moment your application registers an OpenTelemetry SDK (Sentry, Datadog, NodeSDK, etc.); zero runtime cost when no SDK is registered. Configurable through
forRoot({ otel: ... }). - Header contract — formal documentation of the NATS message headers the transport reads and writes. Use this as the integration spec when publishing to (or consuming from) the transport from other languages (Go, Python, etc.).
Peer dependency (optional):
@opentelemetry/apiis now declared as an optional peer dependency. Applications that already register an OpenTelemetry SDK (@sentry/node,@datadog/tracer,@opentelemetry/sdk-node, etc.) bring it in transitively — no action needed. Applications that want to use the library's distributed-tracing feature and do not already have an OTel SDK must install@opentelemetry/apiat the same major version (^1.9.0). This avoids the silent "no-op tracer" trap where two copies of the API live innode_modulesand the global tracer singleton refuses the mismatched version.
Behavior changes (non-breaking API, observable in logs/APM):
- Reduced internal logging. Several
logger.errorsites that duplicated existingTransportHooksevents have been removed. The hook is now the single observability channel for those events. If you relied on those log lines for monitoring, register the relevant hook (error,rpcTimeout,deadLetter). - Error classification on OTel spans. When OpenTelemetry is enabled, handler throws of
RpcExceptionandHttpExceptionproduceOKspans withjetstream.rpc.reply.has_errorandjetstream.rpc.reply.error.codeattributes — they are treated as expected business outcomes per the RPC contract. BareErrorthrows produceERRORspans withrecordException. This keeps APM error rates clean for known business denials while loud-failing on real bugs. - TransportEvent.Error now fires on every handler throw, both event and RPC paths. Previously only some paths emitted it. If you have an
errorhook registered, it will now receive these (which is what most users want). RpcConfig.timeoutin JetStream mode now boundsconnect + RPC. Previously the JetStream-mode per-request deadline only started afterawait connect()resolved, which meant a permanent NATS outage could accumulate pending RPCs indefinitely. The deadline is now armed immediately so callers always see a timeout. Core-mode RPC still relies onnats.js's ownnc.request({ timeout }), which starts afterconnect()resolves; operators running permanent-outage scenarios against Core mode should configuremaxReconnectAttemptsto stop the retry loop at the protocol layer.
No breaking API changes. Existing applications upgrade by bumping the dependency.
v2.8 → v2.9
Notable change:
max_age reduced: 1 day → 1 hourBroadcast messages (config propagation, cache invalidation, feature flags) are relevant for minutes, not days. The new default provides a sufficient catch-up window while reducing storage. This is a mutable property — existing streams update automatically on next application startup. If you need a longer retention window, override it explicitly:
broadcast: { stream: { max_age: toNanos(1, 'days') } }
New features:
- Built-in Dead Letter Queue stream — add the
dlq: { stream }option toforRoot()and the library provisions a dedicated DLQ stream on startup. Exhausted messages are automatically republished with tracking headers (x-dead-letter-reason,x-original-subject,x-original-stream,x-failed-at,x-delivery-count). TheonDeadLettercallback remains available as a standalone option or as a notification/safety-net hook when combined with the DLQ stream. - Per-message TTL —
JetstreamRecordBuilder.ttl(duration)sets theNats-TTLheader (NATS 2.11+), allowing individual messages to expire independently of the stream'smax_age. Requiresallow_msg_ttl: trueon the target stream. - Handler metadata registry (NATS KV) — when enabled via the
metadataoption, the library publishes all registered@EventPattern/@MessagePatternhandlers to a NATS KV bucket at startup, enabling cross-service discovery without a separate service registry. - Stream migration — automatic blue-green stream recreation for immutable property changes (
storage,retention, etc.). Enable withallowDestructiveMigration: trueon a per-stream basis. - Consumer self-healing auto-recreation — consumers deleted externally (via NATS CLI, cluster issues) are automatically recreated on the next poll. Migration-aware: waits during active stream migrations.
StreamConfigOverridestype — prevents users from overridingretention(transport-controlled).NatsErrorCodeenum for NATS JetStream API error codes, so error handling code can switch on typed constants instead of magic strings.
No breaking changes.
v2.7 → v2.8
Breaking change: migrated from nats package to @nats-io/* scoped packages (v3.x).
This is an internal change — the library re-exports everything users need. If you import types directly from nats in your own code, update them:
- import { JsMsg, NatsConnection } from 'nats';
+ import { JsMsg } from '@nats-io/jetstream';
+ import { NatsConnection } from '@nats-io/transport-node';
New features:
- Message scheduling — one-shot delayed delivery via
scheduleAt()(requires NATS >= 2.12) allow_msg_schedulesstream config option
v2.6 → v2.7
v2.7 shipped handler-controlled settlement — a way to ack, nak, or terminate a message without throwing.
New features:
- Handler-controlled settlement via
ctx.retry()andctx.terminate()— control message acknowledgment without throwing errors - Metadata getters on
RpcContext:getDeliveryCount(),getStream(),getSequence(),getTimestamp(),getCallerName()
No breaking changes.
v2.5 → v2.6
v2.6 was the performance release — concurrency control, ack extension, and production-ready defaults for reconnection and compression.
New features:
- Configurable concurrency for event/broadcast/RPC processing
- Ack extension (
ackExtension: true) for long-running handlers - Consume options passthrough for advanced prefetch tuning
- Heartbeat monitoring with automatic consumer restart
- S2 stream compression enabled by default (see Stream Defaults)
- Performance connection defaults (unlimited reconnect, 1s interval)
No breaking changes.
v2.4 → v2.5
Breaking change: nanos() renamed to toNanos().
- import { nanos } from '@horizon-republic/nestjs-jetstream';
+ import { toNanos } from '@horizon-republic/nestjs-jetstream';
consumer: {
- ack_wait: nanos(30, 'seconds'),
+ ack_wait: toNanos(30, 'seconds'),
}
v2.3 → v2.4
New features:
- Ordered events (
ordered:prefix,DeliverPolicyoptions) - Custom message IDs via
setMessageId()for publish-side deduplication - Documentation site (Docusaurus)
No breaking changes.
v2.1 → v2.2
New features:
- Dead letter queue support via
onDeadLettercallback DeadLetterInfointerface with full message context
No breaking changes.
See also
- Installation — setup requirements
- Module Configuration — full options reference
- Quick Start — first handler in 5 minutes