{"id":50302170,"url":"https://github.com/softwarity/nestjs-amqp","last_synced_at":"2026-05-28T13:30:19.407Z","repository":{"id":360900042,"uuid":"1251234782","full_name":"softwarity/nestjs-amqp","owner":"softwarity","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-28T11:14:51.000Z","size":475,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T11:18:31.221Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://softwarity.github.io/nestjs-amqp/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/softwarity.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-27T11:27:50.000Z","updated_at":"2026-05-28T11:14:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/softwarity/nestjs-amqp","commit_stats":null,"previous_names":["softwarity/nestjs-amqp"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/softwarity/nestjs-amqp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-amqp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-amqp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-amqp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-amqp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/softwarity","download_url":"https://codeload.github.com/softwarity/nestjs-amqp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softwarity%2Fnestjs-amqp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33611247,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-28T13:30:18.471Z","updated_at":"2026-05-28T13:30:19.386Z","avatar_url":"https://github.com/softwarity.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @softwarity/nestjs-amqp\n\n[![npm version](https://img.shields.io/npm/v/@softwarity/nestjs-amqp.svg)](https://www.npmjs.com/package/@softwarity/nestjs-amqp)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Node](https://img.shields.io/node/v/@softwarity/nestjs-amqp.svg)](https://nodejs.org)\n\n**AMQP 1.0 integration for NestJS, powered by [rhea](https://github.com/amqp/rhea).** A thin, RxJS-friendly wrapper that exposes decorator-based publishers and consumers — designed for RabbitMQ 4.x (native AMQP 1.0), Apache ActiveMQ Artemis, Apache Qpid, and Azure Service Bus.\n\n📚 **Full documentation:** [softwarity.github.io/nestjs-amqp](https://softwarity.github.io/nestjs-amqp/)\n\n---\n\n\u003e ## ⚠ Read this before your first deploy\n\u003e\n\u003e **This library does NOT create topology at runtime.** It opens senders and receivers on destinations that **must already exist** on the broker — queues, streams, exchanges, DLX bindings, the lot. Missing topology = silent failure (the AMQP link is rejected with `amqp:not-found`; the rest of the connection stays up and the app looks healthy).\n\u003e\n\u003e Declare everything broker-side via a definitions file or an IaC script. Full examples for **RabbitMQ 4.x** (`definitions.json` + docker-compose), **ActiveMQ Artemis** (`broker.xml`), **Azure Service Bus** (Azure CLI), and **Apache Qpid** live on the [doc site](https://softwarity.github.io/nestjs-amqp/#/broker-topology).\n\n---\n\n## Why?\n\n`@nestjs/microservices` only covers AMQP 0.9.1 (via `amqplib`). When you want **AMQP 1.0** features — long-lived sessions, link credit, source filters, message annotations, stream consumers — `rhea` is the canonical Node.js client. This library wraps rhea so the rest of your codebase only sees `@AmqpQueue`, `@Consume`, and Observables.\n\n## Features\n\n- 🎯 **Decorator-based** publishers (`@AmqpQueue`, `@AmqpTopic`) and consumers (`@Consume`, `@Subscribe`)\n- 🌐 **Multi-broker** — speak to several brokers from one service; one connection / reply stream / DLQ per broker\n- 🔄 **Request/Reply** via per-process correlation prefix on a shared reply stream (opt-in)\n- 📡 **Broadcast/PubSub** via RabbitMQ streams (`@Subscribe`)\n- 🔁 **Built-in retry policy** (`maxDelivery`, `dlq`) on work-queue consumers (opt-in)\n- 💀 **Optional DLQ browser** — paginate, replay, drop dead-lettered messages\n- 🧬 **Pluggable wire codec** — JSON by default with `Date` round-trip + ObjectId auto-rehydration; bring your own per broker (msgpack, protobuf, …)\n- 🔧 **`forRoot` / `forRootAsync`** configuration\n- ⚛️ **RxJS-native** — no Promise wrapper, no axios-style imperative shapes\n\n## Installation\n\n```bash\nnpm install @softwarity/nestjs-amqp rhea\n# peer deps you probably already have\nnpm install @nestjs/common @nestjs/core rxjs reflect-metadata\n```\n\n---\n\n# Getting started — the 90% case\n\nThe simplest, most common setup: **one broker, fire-and-forget publish, basic consume — no DLQ, no request/reply**. Declare as many queues and topics as you need; the simplification here is the feature surface, not the quantity. Reply/DLQ are opt-in features documented further down.\n\n### 1. Declare your queues and topics broker-side\n\nThe library never declares topology — only opens senders/receivers on destinations that already exist. Declare whatever your service needs (one queue, ten queues, mixed work-queues and broadcast streams — same exercise). With RabbitMQ 4.x via `definitions.json`:\n\n```json\n{\n  \"queues\": [\n    {\n      \"name\": \"orders.create\",\n      \"vhost\": \"/\",\n      \"durable\": true,\n      \"auto_delete\": false,\n      \"arguments\": { \"x-queue-type\": \"quorum\" }\n    },\n    {\n      \"name\": \"orders.ship\",\n      \"vhost\": \"/\",\n      \"durable\": true,\n      \"auto_delete\": false,\n      \"arguments\": { \"x-queue-type\": \"quorum\" }\n    },\n    {\n      \"name\": \"changes.bulletin\",\n      \"vhost\": \"/\",\n      \"durable\": true,\n      \"auto_delete\": false,\n      \"arguments\": { \"x-queue-type\": \"stream\", \"x-max-age\": \"1h\" }\n    }\n  ]\n}\n```\n\nQuorum queues for work-queue semantics (one consumer per message), stream queues for broadcast (every consumer sees every message). The library makes no assumption about how many you declare.\n\n### 2. Register the module\n\n```ts\nimport { Module } from '@nestjs/common';\nimport { AmqpModule } from '@softwarity/nestjs-amqp';\n\n@Module({\n  imports: [\n    AmqpModule.forRoot({\n      url: 'amqp://localhost:5672',\n      username: 'guest',\n      password: 'guest',\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\nA single broker (the name is implicit — internally `'default'`). Because only one broker is configured, the `brokerName` argument is optional on every decorator and on the locator — the library resolves the lone broker automatically. If you want a custom name (visible as the AMQP container ID on the broker management UI), wrap in an array even with one entry: `AmqpModule.forRoot([{ name: 'my-svc', url, ... }])`.\n\n### 3. Publish — fire and forget\n\n```ts\nimport { Injectable } from '@nestjs/common';\nimport { AmqpQueue, AmqpTopic } from '@softwarity/nestjs-amqp';\n\n@Injectable()\nexport class OrdersService {\n  @AmqpQueue('orders.create')\n  private readonly create!: AmqpQueue\u003cOrderBody\u003e;\n\n  @AmqpQueue('orders.ship')\n  private readonly ship!: AmqpQueue\u003cOrderShipped\u003e;\n\n  @AmqpTopic('changes.bulletin')\n  private readonly changes!: AmqpTopic\u003cBulletinChange\u003e;\n\n  newOrder(body: OrderBody): void {\n    this.create.emit(body);                       // fire-and-forget\n  }\n\n  notifyShipped(body: OrderShipped): void {\n    this.ship.emit(body);\n    this.changes.emit({ type: 'shipped', orderId: body.id, when: new Date().toISOString() });\n  }\n}\n```\n\n`@AmqpQueue` for work-queues (point-to-point) and `@AmqpTopic` for broadcast. `emit()` returns synchronously a `boolean` — `true` if the message was handed off to the sender, `false` if the broker is disabled or not connected. The boolean lets the caller fall back (e.g. NestJS `EventEmitter2` for in-process delivery, a local outbox, …):\n\n```ts\nif (!this.orders.emit(body)) {\n  this.bus.emit('orders.create', body);   // in-process fallback\n}\n```\n\nEach handle is generic on the payload type — every call site is type-checked at compile time.\n\n### 4. Consume\n\n```ts\nimport { Injectable } from '@nestjs/common';\nimport { Consume, Subscribe } from '@softwarity/nestjs-amqp';\n\n@Injectable()\nexport class OrdersListener {\n  // The single un-annotated argument is bound to the JSON-decoded body.\n  // Equivalent to writing @AmqpBody() explicitly.\n  @Consume('orders.create')\n  onCreate(order: OrderBody): void {\n    this.svc.handle(order);\n  }\n\n  @Consume('orders.ship')\n  onShip(shipped: OrderShipped): void {\n    this.svc.markShipped(shipped);\n  }\n\n  @Subscribe('changes.bulletin')\n  onChange(change: BulletinChange): void {\n    this.realtime.publish(change);\n  }\n}\n```\n\nStart the app — you'll see a boot log section like `broker 'default': 3 consumer(s)` followed by one line per binding (each tagged `@Consume` or `@Subscribe`). You're done.\n\n### What's NOT in the 90% case\n\nThe bootstrap above intentionally skips three optional features. Add them à la carte:\n\n| Feature | What you gain | What you have to do |\n|---|---|---|\n| [Request / reply (`send()`)](#request--reply--opt-in) | Wait for a reply Observable — RPC-style. | Declare a stream queue broker-side, add `replyStreamAddress` to the broker config. |\n| [Retry \u0026 DLQ](#retry--dlq--opt-in) | Auto-retry on handler error, then route the failed message to a DLQ. | Declare a DLX + DLQ broker-side, set `{ maxDelivery, dlq: true }` on the decorator. |\n| [Multiple brokers](#multi-broker) | Speak to several brokers from one service. | Pass an array to `forRoot`, pass `brokerName` on each decorator. |\n\n---\n\n# Request / reply — opt-in\n\n`AmqpQueue.send()` returns an `Observable` that resolves with the peer's reply. It needs three things:\n\n### 1. Declare a stream queue broker-side\n\n```json\n{\n  \"queues\": [{\n    \"name\": \"my-service.replies\",\n    \"vhost\": \"/\",\n    \"durable\": true,\n    \"auto_delete\": false,\n    \"arguments\": { \"x-queue-type\": \"stream\", \"x-max-age\": \"5m\" }\n  }]\n}\n```\n\n### 2. Set `replyStreamAddress` on the broker options\n\n```ts\nAmqpModule.forRoot({\n  url: 'amqp://localhost:5672',\n  username: 'guest', password: 'guest',\n  replyStreamAddress: 'my-service.replies',   // ← REQUIRED for send()\n});\n```\n\n### 3. Call `send()` on the publisher side\n\n```ts\ncreateOrder(body: OrderBody): Observable\u003cOrderConfirmation\u003e {\n  return this.orders.send\u003cOrderConfirmation\u003e(body, { timeoutMs: 5000 });\n}\n```\n\n### 4. Return a value from the consumer to auto-reply\n\n```ts\n@Consume('orders.create')\nonCreate(body: OrderBody): Observable\u003cOrderConfirmation\u003e {\n  return this.svc.create(body);   // resolved value -\u003e auto-shipped on reply_to\n}\n```\n\nThe library generates a per-process correlation prefix at boot and filters incoming replies on the shared reply stream — every instance sees every reply but only routes its own. Trade-off: N× bandwidth per reply (negligible for low-volume RPC on a LAN).\n\nWithout `replyStreamAddress` set on the broker, `send()` throws `AmqpConnectionError` at the call site. `emit()` and `@Consume` continue to work unchanged.\n\n📚 Full details: [doc site → Request / reply](https://softwarity.github.io/nestjs-amqp/#/request-reply)\n\n---\n\n# Retry \u0026 DLQ — opt-in\n\nRetry and DLQ are off by default (`maxDelivery: 1`, `dlq: false`) — handler errors silently drop the message.\n\n\u003e **The lib never publishes to a DLQ itself.** On terminal failure with `dlq: true`, it calls `delivery.reject()` and the **broker** routes the message via its own DLX configuration. If the queue has no DLX broker-side, `dlq: true` is silently ignored (the broker discards rejected messages).\n\n### Setup with RabbitMQ 4.x\n\n**1. Declare DLX + DLQ broker-side:**\n\n```json\n{\n  \"exchanges\": [{\n    \"name\": \"my-service.dlx\",\n    \"vhost\": \"/\",\n    \"type\": \"direct\",\n    \"durable\": true,\n    \"auto_delete\": false\n  }],\n\n  \"queues\": [\n    {\n      \"name\": \"payments.process\",\n      \"vhost\": \"/\",\n      \"durable\": true,\n      \"arguments\": {\n        \"x-queue-type\": \"quorum\",\n        \"x-dead-letter-exchange\": \"my-service.dlx\",\n        \"x-dead-letter-routing-key\": \"payments.process\"\n      }\n    },\n    {\n      \"name\": \"my-service.dlq\",\n      \"vhost\": \"/\",\n      \"durable\": true,\n      \"arguments\": { \"x-queue-type\": \"quorum\" }\n    }\n  ],\n\n  \"bindings\": [{\n    \"source\": \"my-service.dlx\",\n    \"vhost\": \"/\",\n    \"destination\": \"my-service.dlq\",\n    \"destination_type\": \"queue\",\n    \"routing_key\": \"payments.process\",\n    \"arguments\": {}\n  }]\n}\n```\n\n**2. Set `defaultDlqAddress` on the broker options** (used by the DLQ admin UI as a pre-fill):\n\n```ts\nAmqpModule.forRoot({\n  url: 'amqp://localhost:5672',\n  username: 'guest', password: 'guest',\n  defaultDlqAddress: 'my-service.dlq',\n});\n```\n\n**3. Enable the policy on the decorator:**\n\n```ts\n@Consume('payments.process', { maxDelivery: 5, dlq: true })\nonPayment(body: Payment): Observable\u003cResult\u003e {\n  return this.svc.process(body);\n}\n```\n\nRun-time behaviour: handler throws → `modified(delivery_failed:true)` × 4 retries → on the 5th failure → `reject()` → broker routes to `my-service.dlx` with routing key `payments.process` → `my-service.dlq`.\n\n### `retryPolicy` — delayed retries (in 0.3.x)\n\nThe decorator accepts a `retryPolicy` option that defines the timing between retries. **In 0.2.x only `'immediate'` is functional** — `fixed` / `exponential` shapes are accepted by the type system for forward-compatibility (runtime falls back to immediate with a boot warning). Client-side scheduled republish is planned for 0.3.x.\n\n```ts\ntype RetryPolicy =\n  | 'immediate'\n  | { kind: 'fixed';       delayMs: number }\n  | { kind: 'exponential'; initialMs: number; multiplier: number; maxMs: number };\n```\n\n📚 Full details: [doc site → Retry \u0026 DLQ](https://softwarity.github.io/nestjs-amqp/#/retry-and-dlq)\n\n---\n\n# Multi-broker\n\nPass an array to `forRoot` and pass the broker name on each decorator. Each broker is independent — its own connection, reply stream, DLQ, body codec, enabled flag.\n\n```ts\nAmqpModule.forRoot([\n  {\n    name: 'primary',\n    url: 'amqp://broker-a:5672',\n    username: 'svc', password: '...',\n    replyStreamAddress: 'my-svc.replies',\n    defaultDlqAddress: 'my-svc.dlq',\n  },\n  {\n    name: 'analytics',\n    url: 'amqp://broker-b:5672',\n    username: 'svc', password: '...',\n    enabled: false,                       // per-broker kill switch\n    // No reply stream / DLQ — analytics is emit-only.\n  },\n]);\n\n@Injectable()\nexport class MixedService {\n  @AmqpQueue('orders.create', 'primary')        private orders!: AmqpQueue\u003cOrderBody\u003e;\n  @AmqpTopic('metrics.collected', 'analytics')  private metrics!: AmqpTopic\u003cMetric\u003e;\n}\n\n@Injectable()\nexport class MixedListener {\n  @Consume('orders.create', 'primary', { dlq: true })\n  onOrder(o: OrderBody): void { ... }\n\n  @Subscribe('events.tick', 'analytics')\n  onTick(e: TickEvent): void { ... }\n}\n```\n\nThe 2nd argument on `@Consume` / `@Subscribe` is detected at runtime — string = broker name, object = options. The forms `(addr)`, `(addr, options)`, `(addr, brokerName)`, `(addr, brokerName, options)` are all valid.\n\nForgetting the broker name in a multi-broker setup throws clearly at boot.\n\n📚 Full details: [doc site → Multi-broker](https://softwarity.github.io/nestjs-amqp/#/multi-broker)\n\n---\n\n## Quick reference\n\n### Decorators\n\n```ts\n@AmqpQueue(address, brokerName?)        // Property → AmqpQueue\u003cT\u003e (emit + send)\n@AmqpTopic(address, brokerName?)        // Property → AmqpTopic\u003cT\u003e (emit only)\n\n@Consume(address, brokerName?, options?)        // Method, work-queue consumer\n@Subscribe(address, brokerName?, options?)   // Method, stream/topic consumer\n```\n\n`brokerName` is optional when a single broker is configured. With several brokers, omitting it throws at boot.\n\n### Parameter decorators\n\n```ts\n@AmqpBody()                // T — decoded body (also: a single un-annotated param is implicit @AmqpBody())\n@AmqpAddress()             // string — the @Subscribe address\n@AmqpDeliveryCount()       // number — 1-based attempt count\n@AmqpHeader()              // MessageHeader — durable, priority, ttl, delivery_count\n@AmqpProperties()          // MessageProperties — full standard properties\n@AmqpProperty(name)        // one field of message.properties\n@AmqpAppProperties()       // Record\u003cstring, unknown\u003e — full application_properties\n@AmqpAppProperty(name)     // one field of application_properties\n@AmqpSettler()             // AmqpSettler — manual accept/release/reject\n@AmqpContext()             // AmqpContext — full envelope + settle helpers\n```\n\n### Runtime resolution — `AmqpDestinations`\n\nInject `AmqpDestinations` to resolve a publish handle dynamically (tenant-scoped queues, dispatchers):\n\n```ts\n@Injectable()\nexport class DynamicPublisher {\n  constructor(private readonly amqp: AmqpDestinations) {}\n\n  publish(tenantId: string, body: OrderBody): void {\n    this.amqp.queue\u003cOrderBody\u003e(`orders.${tenantId}`).emit(body);\n  }\n}\n```\n\n### DLQ browser — `DlqAdminModule` (opt-in)\n\n```ts\n@Module({\n  imports: [\n    AmqpModule.forRoot({ url: '...', /* ... */ }),\n    DlqAdminModule,   // adds /admin/dlq/... routes\n  ],\n})\nexport class AppModule {}\n```\n\nRoutes (single-broker shortcut):\n\n```\nPOST /admin/dlq/sessions                            { dlqAddress, pageSize? }\nGET  /admin/dlq/sessions/:token\nPOST /admin/dlq/sessions/:token/next-page\nPOST /admin/dlq/sessions/:token/messages/:idx/replay\nPOST /admin/dlq/sessions/:token/messages/:idx/drop\nPOST /admin/dlq/sessions/:token/close\n```\n\nMulti-broker variant: `POST /admin/dlq/:broker/sessions { ... }` to scope the open-session to a specific broker. Other routes work off the session token (the session knows its broker).\n\n**⚠️ Auth not included.** The controller is unguarded — wrap with your own `Guard`, or sub-class and redeclare with your decorators. `openedBy` is read from `req.user.username ?? req.user.id ?? 'anonymous'`.\n\n### Serialization / Deserialization — per broker\n\n```ts\nAmqpModule.forRoot([\n  { name: 'primary',   url: '...', /* default JSON codec */ },\n  { name: 'analytics', url: '...', bodyCodec: new MsgpackCodec() },\n]);\n```\n\nDefault `JsonBodyCodec`:\n- UTF-8 JSON\n- Round-trips `Date` via `{ \"$date\": \"\u003cISO\u003e\" }`\n- Encodes ObjectId-like values as `{ \"$oid\": \"\u003chex\u003e\" }`; **decode auto-detects mongoose / bson and returns a real ObjectId instance** if installed, else the marker object\n\n### Errors\n\n| Class | Where it surfaces |\n|---|---|\n| `AmqpConnectionError` | Connection-level issues, `send()` when AMQP is disabled or no reply stream is configured on the broker |\n| `AmqpTimeoutError` | `send()` Observable when no reply arrives in time. Carries `address`, `correlationId`, `timeoutMs` |\n| `AmqpHandlerError` | Reserved for future use |\n| `AmqpError` | Abstract base — `if (err instanceof AmqpError) …` |\n\n## Known limitations\n\n- **In-flight `send()` across reconnects** — if a reconnect happens between sending and receiving the reply, the reply is lost (we re-subscribe with `streamOffset: 'next'`). The pending call times out.\n- **`topic.send()` (scatter-gather RPC)** — not supported. Build aggregation in user code on top of `emit()` if needed.\n- **`@Subscribe` replay** — hardcoded to `streamOffset: 'next'`. PR welcome for a dedicated `@SubscribeStream` exposing the option.\n- **Delayed retry (`retryPolicy`)** — only `'immediate'` is functional in 0.2.x. `fixed` / `exponential` shapes accepted by the type system; runtime falls back to immediate with a boot warning.\n\n## License\n\nMIT © François ACHACHE\n\n## Contributing\n\nPRs welcome. Run `npm test \u0026\u0026 npm run lint \u0026\u0026 npm run build` before submitting.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoftwarity%2Fnestjs-amqp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoftwarity%2Fnestjs-amqp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoftwarity%2Fnestjs-amqp/lists"}