@bgaldino/nestjs-rabbitmq 1.5.1 → 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +432 -322
  2. package/dist/amqp-connection-manager.d.ts +15 -18
  3. package/dist/amqp-connection-manager.js +98 -143
  4. package/dist/amqp-connection-manager.js.map +1 -1
  5. package/dist/class-discovery.d.ts +13 -0
  6. package/dist/class-discovery.js +78 -0
  7. package/dist/class-discovery.js.map +1 -0
  8. package/dist/connection-factory.d.ts +20 -0
  9. package/dist/connection-factory.js +106 -0
  10. package/dist/connection-factory.js.map +1 -0
  11. package/dist/helper.d.ts +1 -1
  12. package/dist/helper.js +3 -7
  13. package/dist/helper.js.map +1 -1
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.js +8 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/rabbit-consumer.decorator.d.ts +7 -0
  18. package/dist/rabbit-consumer.decorator.js +13 -0
  19. package/dist/rabbit-consumer.decorator.js.map +1 -0
  20. package/dist/rabbitmq-consumer.d.ts +19 -0
  21. package/dist/rabbitmq-consumer.js +183 -0
  22. package/dist/rabbitmq-consumer.js.map +1 -0
  23. package/dist/rabbitmq-retry-handler.d.ts +10 -0
  24. package/dist/rabbitmq-retry-handler.js +46 -0
  25. package/dist/rabbitmq-retry-handler.js.map +1 -0
  26. package/dist/rabbitmq-service.d.ts +12 -9
  27. package/dist/rabbitmq-service.js +45 -37
  28. package/dist/rabbitmq-service.js.map +1 -1
  29. package/dist/rabbitmq.constants.d.ts +1 -0
  30. package/dist/rabbitmq.constants.js +5 -0
  31. package/dist/rabbitmq.constants.js.map +1 -0
  32. package/dist/rabbitmq.interfaces.d.ts +32 -10
  33. package/dist/rabbitmq.module.d.ts +7 -7
  34. package/dist/rabbitmq.module.js +53 -7
  35. package/dist/rabbitmq.module.js.map +1 -1
  36. package/dist/rabbitmq.types.d.ts +67 -29
  37. package/dist/rabbitmq.types.js +4 -0
  38. package/dist/rabbitmq.types.js.map +1 -1
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +12 -10
  41. package/dist/rabbitmq-consumers.d.ts +0 -19
  42. package/dist/rabbitmq-consumers.js +0 -210
  43. package/dist/rabbitmq-consumers.js.map +0 -1
package/README.md CHANGED
@@ -1,489 +1,599 @@
1
1
  # @bgaldino/nestjs-rabbitmq
2
2
 
3
- # Table of Contents
4
-
5
- <!--toc:start-->
6
-
7
- - [@bgaldino/nestjs-rabbitmq](#bgaldinonestjs-rabbitmq)
8
- - [Table of Contents](#table-of-contents)
9
- - [Description](#description)
10
- - [Motivation](#motivation)
11
- - [Requirements](#requirements)
12
- - [Instalation](#instalation)
13
- - [PNPM](#pnpm)
14
- - [Getting Started](#getting-started)
15
- - [Importing the module](#importing-the-module)
16
- - [The configuration file](#the-configuration-file)
17
- - [Publishers](#publishers)
18
- - [Publishing messages](#publishing-messages)
19
- - [Custom headers](#custom-headers)
20
- - [Consumers](#consumers)
21
- - [The messageHandler callback](#the-messagehandler-callback)
22
- - [Strongly typed consumer](#strongly-typed-consumer)
23
- - [Declaration example](#declaration-example)
24
- - [Retrying strategy](#retrying-strategy)
25
- - [Deadletter strategy](#deadletter-strategy)
26
- - [Disabling the automatic ack](#disabling-the-automatic-ack)
27
- - [Custom Header metadata](#custom-header-metadata)
28
- - [Extra options](#extra-options)
29
- - [Consumer manual loading](#consumer-manual-loading)
30
- - [Message inspection and logging](#message-inspection-and-logging)
31
- - [How to build this library locally?](#how-to-build-this-library-locally)
32
- - [Planned features](#planned-features)
33
- - [Contribute](#contribute)
34
- - [License](#license)
35
- <!--toc:end-->
36
-
37
- ## Description
38
-
39
- This module features an opinionated way of configuring the RabbitMQ connection
40
- by using a configuration file that describes the behaviour of all publishers
41
- and consumers present in your project
42
-
43
- ## Motivation
44
-
45
- I wanted to have a central place where its easier to open the project and see
46
- the declared behaviour of the RabbitMQ instance for that project, without having
47
- to look around for NestJS annotations, microservices or whatever. Simply to open,
48
- go to the configuration file and know exactly what I'm looking at and what I should
49
- expect.
50
-
51
- ### Requirements
52
-
53
- - @nestjs/common: ">9"
54
- - An RabbitMQ instance with the
55
- [RabbitMQ Delayed Message Plugin](https://github.com/rabbitmq/rabbitmq-delayed-message-exchange) installed
56
-
57
- ### Instalation
58
-
59
- ### PNPM
3
+ An opinionated NestJS module for RabbitMQ with built-in retry strategies, dead letter queues, and quorum queue support.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Requirements](#requirements)
9
+ - [Getting Started](#getting-started)
10
+ - [forRoot](#forroot)
11
+ - [forRootAsync](#forrootasync)
12
+ - [Consumers](#consumers)
13
+ - [Decorator-based consumers](#decorator-based-consumers)
14
+ - [Config-based consumers](#config-based-consumers)
15
+ - [Mixed usage](#mixed-usage)
16
+ - [Handler signature](#handler-signature)
17
+ - [Consumer groups](#consumer-groups)
18
+ - [Publishers](#publishers)
19
+ - [Publishing messages](#publishing-messages)
20
+ - [Typed publishing](#typed-publishing)
21
+ - [Multi-vhost Connections](#multi-vhost-connections)
22
+ - [Named connections](#named-connections)
23
+ - [Targeting a connection from consumers](#targeting-a-connection-from-consumers)
24
+ - [Publishing to a specific connection](#publishing-to-a-specific-connection)
25
+ - [Retry Strategy](#retry-strategy)
26
+ - [Dead Letter Strategy](#dead-letter-strategy)
27
+ - [Disabling the automatic ack](#disabling-the-automatic-ack)
28
+ - [Custom Header Metadata](#custom-header-metadata)
29
+ - [Extra Options](#extra-options)
30
+ - [Consumer manual loading](#consumer-manual-loading)
31
+ - [Message inspection and logging](#message-inspection-and-logging)
32
+ - [Health check](#health-check)
33
+ - [Building locally](#building-locally)
34
+ - [License](#license)
35
+
36
+ ## Installation
60
37
 
61
38
  ```shell
62
39
  pnpm add @bgaldino/nestjs-rabbitmq
63
40
  ```
64
41
 
65
- **YARN**
66
-
67
42
  ```shell
68
43
  yarn add @bgaldino/nestjs-rabbitmq
69
44
  ```
70
45
 
71
- **NPM**
72
-
73
46
  ```shell
74
47
  npm add @bgaldino/nestjs-rabbitmq
75
48
  ```
76
49
 
50
+ ## Requirements
51
+
52
+ - `@nestjs/common` and `@nestjs/core` version 9 or above
53
+ - RabbitMQ 3.10+ (quorum queue per-message TTL support)
54
+
55
+ No additional plugins are required. The retry mechanism uses native message TTL
56
+ and dead letter exchanges instead of the delayed message plugin.
57
+
77
58
  ## Getting Started
78
59
 
79
- ### Importing the module
60
+ The `RabbitMQModule` is marked as `@Global`, so importing it once is enough
61
+ to inject `RabbitMQService` anywhere in your application.
62
+
63
+ ### forRoot
80
64
 
81
- Import the `RabbitMQModule` in your `app.module.ts` and call the method `register({})`
65
+ For simple setups where the configuration is static:
82
66
 
83
67
  ```typescript
84
68
  import { RabbitMQModule } from '@bgaldino/nestjs-rabbitmq';
85
- import { RabbitOptions } from '@bgaldino/nestjs-rabbitmq';
86
69
 
87
70
  @Module({
88
71
  imports: [
89
- ...
90
- RabbitMQModule.register({ useClass: RabbitOptions, injects: [...] }),
91
- ...
92
- ]
72
+ RabbitMQModule.forRoot({
73
+ connectionString: 'amqp://user:password@localhost:5672/vhost',
74
+ delayExchangeName: 'my_app',
75
+ assertExchanges: [
76
+ { name: 'orders', type: 'topic' },
77
+ { name: 'notifications', type: 'fanout' },
78
+ ],
79
+ }),
80
+ ],
93
81
  })
94
82
  export class AppModule {}
95
83
  ```
96
84
 
97
- The `RabbitMQModule` is marked as `@Global`, therefore, calling it once is enough
98
- to allow the injection of the `RabbitMQService`
85
+ ### forRootAsync
99
86
 
100
- ### The configuration file
101
-
102
- Create a `rabbitmq.config.ts` or whatever the name you prefer containing the
103
- minimum configuration:
87
+ When you need to inject dependencies or resolve configuration asynchronously:
104
88
 
105
89
  ```typescript
106
- import { Injectable } from "@nestjs/common";
107
- import {
108
- RabbitMQModuleOptions,
109
- RabbitOptionsFactory,
110
- } from "@bgaldino/nestjs-rabbitmq";
90
+ import { RabbitMQModule, RabbitMQOptionsFactory, ModuleOptions } from '@bgaldino/nestjs-rabbitmq';
111
91
 
112
92
  @Injectable()
113
- export class RabbitOptions implements RabbitOptionsFactory {
114
- createRabbitOptions(): RabbitMQModuleOptions {
93
+ class RabbitConfig implements RabbitMQOptionsFactory {
94
+ constructor(private readonly configService: ConfigService) {}
95
+
96
+ createRabbitOptions(): ModuleOptions {
115
97
  return {
116
- connectionString: "amqp://{user}:{password}@{url}/{vhost}",
117
- delayExchangeName: "MyDelayExchange",
118
- assertExchanges: [],
119
- consumerChannels: [],
98
+ connectionString: this.configService.get('RABBIT_URL'),
99
+ delayExchangeName: 'my_app',
100
+ assertExchanges: [
101
+ { name: 'orders', type: 'topic' },
102
+ ],
120
103
  };
121
104
  }
122
105
  }
123
- ```
124
106
 
125
- ## Publishers
107
+ @Module({
108
+ imports: [
109
+ RabbitMQModule.forRootAsync({
110
+ useClass: RabbitConfig,
111
+ imports: [ConfigModule],
112
+ }),
113
+ ],
114
+ })
115
+ export class AppModule {}
116
+ ```
126
117
 
127
- **Example config file**:
118
+ You can also use `useFactory` directly:
128
119
 
129
120
  ```typescript
130
- assertExchanges: [
131
- {
132
- name: 'webhooks', type: 'topic',
133
- options: { durable: true, autoDelete: false }
134
- },
135
- { name: 'test-fanout', type: 'fanout' },
136
- { name: 'example-direct', type: 'direct'},
137
- ],
121
+ RabbitMQModule.forRootAsync({
122
+ useFactory: (configService: ConfigService) => ({
123
+ connectionString: configService.get('RABBIT_URL'),
124
+ delayExchangeName: 'my_app',
125
+ assertExchanges: [],
126
+ }),
127
+ inject: [ConfigService],
128
+ imports: [ConfigModule],
129
+ })
138
130
  ```
139
131
 
140
- The `assertExchanges` property expects an array of `RabbitMQAssertExchange`
141
- and each entry will asserted against the RabbitMQ connected server.
132
+ ## Consumers
142
133
 
143
- If any entry does not match a current configuration, or cannot be
144
- created/attached. A terminal error `406 - PRECONDITION_FAILED` will be thrown
145
- with the reason and the server will not initialize
134
+ There are two ways to register consumers: decorators and config. Both can be
135
+ used simultaneously and produce the same result at runtime. The library
136
+ validates that no duplicate queue names exist across both sources.
146
137
 
147
- ### Publishing messages
138
+ All queues are created as [quorum queues](https://www.rabbitmq.com/docs/quorum-queues)
139
+ by default. Consumers do not create exchanges, they only bind to exchanges
140
+ that already exist (declared via `assertExchanges`).
148
141
 
149
- **Example**:
142
+ ### Decorator-based consumers
143
+
144
+ Decorate any method with `@RabbitConsumer()` and the library will
145
+ automatically discover and wire it up during application bootstrap.
146
+ This is the recommended approach for most use cases since it keeps the
147
+ consumer logic located with the handler.
150
148
 
151
149
  ```typescript
152
- import { Injectable } from "@nestjs/common";
153
- import { RabbitMQService } from "@bgaldino/nestjs-rabbitmq";
150
+ import { Injectable } from '@nestjs/common';
151
+ import { RabbitConsumer, ConsumerOptions, MessageParams } from '@bgaldino/nestjs-rabbitmq';
154
152
 
155
153
  @Injectable()
156
- export class MyService {
157
- constructor(private readonly rabbitMQService: RabbitMQService) {}
158
- }
159
-
160
- async publishMe(){
161
- const isPublished = await this.rabbitMQService.publish('exchange_name', 'routing_key', {});
162
- }
163
-
164
- //or
165
-
166
- async publishMeTyped() {
167
- const isPublished = await this.rabbitMQService.publish<CustomType>('exchange_name', 'routing_key', {});
168
- //This will return an error if the object is not properly typed
154
+ export class OrderService {
155
+ @RabbitConsumer({
156
+ queue: 'order.created',
157
+ exchangeName: 'orders',
158
+ routingKey: 'order.created',
159
+ prefetch: 5,
160
+ })
161
+ async handleOrderCreated(content: OrderPayload, params?: MessageParams) {
162
+ // handle message
163
+ }
169
164
  }
170
165
  ```
171
166
 
172
- The `publish()` method uses [Publish Confirms](https://www.rabbitmq.com/docs/confirms#publisher-confirms)
173
- to make sure that the message is delivered to the broker before returning
174
- the promise.
167
+ The provider must be registered in a NestJS module. The library scans all
168
+ providers and controllers for decorated methods.
175
169
 
176
- ### Custom headers
170
+ ### Config-based consumers
177
171
 
178
- The library defines a couple custom headers
179
-
180
- ## Consumers
181
-
182
- Inside the configuration file you can declare your consumers on the section
183
- `consumerChannels`. This list of `RabbitMQConsumerChannel` will be evaluated
184
- and each entry will try to create the queue and bind it to the declared
185
- exchange.
186
-
187
- **Example:**
172
+ If you prefer centralized configuration, or need to conditionally register
173
+ consumers based on environment, you can use the `consumerChannels` array.
174
+ The `defineRabbitConsumer` helper provides type safety:
188
175
 
189
176
  ```typescript
190
-
191
- createRabbitOptions(): RabbitMQModuleOptions {
192
- return {
193
- ...,
194
- consumerChannels: [
195
- {
196
- options: {
197
- queue: 'myqueue',
198
- exchangeName: 'foobar.exchange',
199
- routingKey: 'myqueue',
200
- prefetch: Number(process.env.RABBIT_PREFETCH ?? 10),
201
- retryStrategy: {
202
- enabled: true,
203
- maxAttempts: 5,
204
- delay: (attempt: number) => {
205
- return attempt * 5000;
206
- },
207
- },
208
- },
209
- messageHandler: this.consumerService.messageHandler.bind(
210
- this.consumerService,
211
- ),
212
- },
213
- ]
214
- }
215
- }
177
+ import { defineRabbitConsumer } from '@bgaldino/nestjs-rabbitmq';
178
+
179
+ RabbitMQModule.forRoot({
180
+ connectionString: 'amqp://localhost',
181
+ delayExchangeName: 'my_app',
182
+ assertExchanges: [{ name: 'orders', type: 'topic' }],
183
+ consumerChannels: [
184
+ defineRabbitConsumer({
185
+ queue: 'order.created',
186
+ exchangeName: 'orders',
187
+ routingKey: 'order.created',
188
+ prefetch: 5,
189
+ handler: {
190
+ provider: OrderService,
191
+ methodName: 'handleOrderCreated',
192
+ },
193
+ }),
194
+ ],
195
+ })
216
196
  ```
217
197
 
218
- The consumer **DOES NOT** create exchanges and only bind to ones that already
219
- exists. This is to avoid creating exchanges with typos and
220
- misconfigurations.
198
+ The library resolves the provider instance automatically. No need to manually
199
+ bind `this` or inject the service into your config.
221
200
 
222
- You can also declare an array of `routingKeys: string[]` if you want to attach
223
- multiple keys to the same queue/callback
201
+ ### Mixed usage
224
202
 
225
- ### The messageHandler callback
203
+ Both approaches can coexist. A common pattern is using decorators for static
204
+ consumers and config for conditional ones:
226
205
 
227
- As declared in the example above, the `messageHandler` property expects a
228
- callback of the type `IRabbitHandler`. Because of the nature of the library,
229
- we will need to call the `.bind(this.yourService)` in order to bind the `this`
230
- context of the origin service to the callback.
206
+ ```typescript
207
+ // Static consumer via decorator
208
+ @RabbitConsumer({ queue: 'audit.log', exchangeName: 'events', routingKey: '#' })
209
+ async auditLog(content: any) { ... }
210
+
211
+ // Conditional consumer via config
212
+ consumerChannels: isProduction ? [
213
+ defineRabbitConsumer({ queue: 'debug.trace', ... })
214
+ ] : [],
215
+ ```
231
216
 
232
- The `RabbitMQModule.register()` accepts an array of NestJS Modules with the
233
- any module that contains an consumer callback function.
217
+ ### Handler signature
234
218
 
235
- The `callback` has the following signature:
219
+ Every consumer handler follows the same signature regardless of registration
220
+ method:
236
221
 
237
222
  ```typescript
238
- async messageHandler(content: any, params?: RabbitConsumerParams): Promise<void>;
223
+ async handler(content: T, params?: MessageParams): Promise<void>;
239
224
  ```
240
225
 
241
- where `RabbitConsumerParams` is optional and contains the following info:
226
+ The `MessageParams` object is optional and contains:
242
227
 
243
228
  ```typescript
244
- export type RabbitConsumerParameters = {
229
+ type MessageParams = {
245
230
  message: ConsumeMessage;
246
231
  channel: ConfirmChannel;
247
232
  queue: string;
233
+ originalRoutingKey?: string;
248
234
  };
249
235
  ```
250
236
 
251
- ### Strongly typed consumer
237
+ The `content` parameter is the parsed message body. The library automatically
238
+ attempts to parse the message as JSON. If parsing fails, the raw string is
239
+ passed instead.
252
240
 
253
- You can use the `IRabbitConsumer<T>` to type the consumer first parameter `content`.
241
+ You can implement the `ConsumerHandler<T>` interface to strongly type your
242
+ consumer:
254
243
 
255
244
  ```typescript
256
- export interface MyInterface {
257
- type: string;
258
- id: number;
259
- }
245
+ import { ConsumerHandler, MessageParams } from '@bgaldino/nestjs-rabbitmq';
260
246
 
261
247
  @Injectable()
262
- export class MyClass implements IRabbitConsumer<MyInterface> {
263
- public async messageHandler(content: MyInterface): Promise<void> {
264
- console.log(content.type);
248
+ export class OrderService implements ConsumerHandler<OrderPayload> {
249
+ async messageHandler(content: OrderPayload, params?: MessageParams): Promise<void> {
250
+ console.log(content.orderId);
265
251
  }
266
252
  }
267
253
  ```
268
254
 
269
- ### Declaration example
255
+ ### Consumer groups
270
256
 
271
- **Service example:**
257
+ Groups allow you to control which consumers are enabled on a given deployment.
258
+ This is useful when multiple instances of the same application serve different
259
+ roles.
272
260
 
273
261
  ```typescript
274
- //consumer.module.ts
275
- @Module({
276
- provides: [ConsumerService],
277
- exports: [ConsumerService],
262
+ @RabbitConsumer({
263
+ queue: 'heavy.processing',
264
+ exchangeName: 'jobs',
265
+ routingKey: 'heavy.*',
266
+ group: 'workers',
278
267
  })
279
- export class ConsumerModule {}
268
+ async processHeavyJob(content: any) { ... }
269
+ ```
270
+
271
+ The active group is determined by:
272
+
273
+ 1. The `RMQ_CONSUMER_GROUP` environment variable (highest priority)
274
+ 2. The `group` parameter passed to `createConsumers(group)`
275
+ 3. Defaults to `"rabbit-default"`
276
+
277
+ Consumers without an explicit group are assigned to the active group and will
278
+ always be initialized. Consumers with a group that does not match the active
279
+ group are skipped.
280
+
281
+ ## Publishers
282
+
283
+ ### Publishing messages
284
+
285
+ Inject `RabbitMQService` and call `publish()`:
286
+
287
+ ```typescript
288
+ import { Injectable } from '@nestjs/common';
289
+ import { RabbitMQService } from '@bgaldino/nestjs-rabbitmq';
280
290
 
281
- //consumer.service.ts
282
291
  @Injectable()
283
- export class ConsumerService {
284
- async messageHandler(content: any) {
285
- return null;
292
+ export class OrderService {
293
+ constructor(private readonly rabbit: RabbitMQService) {}
294
+
295
+ async createOrder(order: Order) {
296
+ const published = await this.rabbit.publish('orders', 'order.created', order);
297
+
298
+ if (!published) {
299
+ // handle publish failure
300
+ }
286
301
  }
287
302
  }
288
303
  ```
289
304
 
290
- **Config Example:**
305
+ The `publish()` method uses [Publisher Confirms](https://www.rabbitmq.com/docs/confirms#publisher-confirms)
306
+ to guarantee the message was delivered to the broker before resolving the
307
+ promise. Returns `true` on success, `false` on failure.
308
+
309
+ ### Typed publishing
310
+
311
+ You can pass a generic type to enforce the message shape at compile time:
291
312
 
292
313
  ```typescript
293
- //rabbit.config.ts
294
- @Injectable()
295
- export class RabbitOptions implements RabbitOptionsFactory {
296
- constructor(
297
- readonly consumerService: ConsumerService ,
298
- ) {}
299
-
300
- createRabbitOptions(): RabbitMQModuleOptions {
301
- return {
302
- ...
303
- consumerchannels: [
304
- {
305
- options: {
306
- queue: "myqueue",
307
- exchangename: "test.exchange",
308
- routingkey: "myqueue",
309
- prefetch: number(process.env.rabbit_prefetch ?? 10),
310
- },
311
- messagehandler: this.MyService.messageHandler.bind(this.MyService),
312
- },
313
- ];
314
- }
315
- }
314
+ await this.rabbit.publish<OrderPayload>('orders', 'order.created', {
315
+ orderId: '123',
316
+ amount: 99.90,
317
+ });
318
+ ```
316
319
 
317
- //app.module.ts
318
- @Module({
319
- imports: [
320
- ...,
321
- RabbitMQModule.register({
322
- useClass: RabbitConfig,
323
- injects: [ConsumerModule]
324
- })
325
- ]
320
+ You can also pass custom publish options as a fourth argument, such as
321
+ additional headers or properties.
322
+
323
+ ## Multi-vhost Connections
324
+
325
+ If your application needs to consume from or publish to multiple RabbitMQ
326
+ vhosts (or entirely different brokers), you can use named connections.
327
+ Each connection is a self-contained unit with its own `connectionString`,
328
+ `delayExchangeName`, `assertExchanges`, and `consumerChannels`.
329
+
330
+ ### Named connections
331
+
332
+ Replace the flat connection fields with a `connections` array. Each entry
333
+ must have a unique `name`:
334
+
335
+ ```typescript
336
+ import { RabbitMQModule, ConnectionConfig } from '@bgaldino/nestjs-rabbitmq';
337
+
338
+ RabbitMQModule.forRoot({
339
+ connections: [
340
+ {
341
+ name: 'default',
342
+ connectionString: 'amqp://localhost/main',
343
+ delayExchangeName: 'my_app',
344
+ assertExchanges: [{ name: 'orders', type: 'topic' }],
345
+ },
346
+ {
347
+ name: 'shared-bus',
348
+ connectionString: 'amqp://localhost/shared',
349
+ delayExchangeName: 'shared_app',
350
+ assertExchanges: [{ name: 'events', type: 'topic' }],
351
+ },
352
+ ],
326
353
  })
327
- export class AppModule {}
328
354
  ```
329
355
 
330
- ## Retrying strategy
356
+ The flat shorthand (`connectionString` at the root level) still works for
357
+ single-connection setups. Internally it becomes a connection named
358
+ `"default"`. You cannot set both `connectionString` and `connections` at the
359
+ same time.
360
+
361
+ ### Targeting a connection from consumers
331
362
 
332
- On each consumer declaration you can pass the optional parameter: `retryStrategy`
333
- with following the contract:
363
+ Add the `connection` field to `@RabbitConsumer()` or to a config-based
364
+ consumer to specify which connection it should attach to. If omitted,
365
+ it defaults to `"default"`:
334
366
 
335
367
  ```typescript
336
- retryStrategy: {
337
- enabled?: true,
338
- maxAttempts?: 5,
339
- delay?: (attempt) => {return attempt * 5000};
340
- }
368
+ @RabbitConsumer({
369
+ queue: 'events.audit',
370
+ exchangeName: 'events',
371
+ routingKey: '#',
372
+ connection: 'shared-bus',
373
+ })
374
+ async auditEvents(content: any) { ... }
375
+ ```
376
+
377
+ Config-based consumers declared inside a connection's `consumerChannels` are
378
+ automatically scoped to that connection:
379
+
380
+ ```typescript
381
+ connections: [
382
+ {
383
+ name: 'default',
384
+ connectionString: 'amqp://localhost/main',
385
+ delayExchangeName: 'my_app',
386
+ assertExchanges: [{ name: 'orders', type: 'topic' }],
387
+ consumerChannels: [
388
+ defineRabbitConsumer({
389
+ queue: 'order.created',
390
+ exchangeName: 'orders',
391
+ routingKey: 'order.created',
392
+ handler: { provider: OrderService, methodName: 'handleOrderCreated' },
393
+ }),
394
+ ],
395
+ },
396
+ ],
341
397
  ```
342
398
 
343
- By default, the `retryStrategy` is enabled. When consuming a new message and
344
- the callback throws an error.
399
+ ### Publishing to a specific connection
400
+
401
+ Pass the `connection` option to `publish()`:
345
402
 
346
- When enabled, the library will create a `{options.queue}.dlq` queue and will use
347
- the `delayExchangeName` exchange as the retrying orchestrator where the
348
- `x-delay` value is the return of the anonymous callback `delay`.
403
+ ```typescript
404
+ await this.rabbit.publish('events', 'audit.created', payload, {
405
+ connection: 'shared-bus',
406
+ });
407
+ ```
349
408
 
350
- When the maximum attempts is reached, the library issues a nack, sending the
351
- message to the `.dlq` queue.
409
+ When omitted, the message is published to the `"default"` connection.
352
410
 
353
- ## Deadletter strategy
411
+ ## Retry Strategy
354
412
 
355
- Each consumer can have an optional parameter `deadletterStrategy` with the
356
- following contract:
413
+ Each consumer can define a `retryStrategy` to handle transient failures. When
414
+ the handler throws an error, the message is published to a retry queue
415
+ (`{queue}.retry`) with a TTL. Once the TTL expires, the message is routed
416
+ back to the original queue for another attempt.
357
417
 
358
418
  ```typescript
359
- retryStrategy: {
360
- suffix: string
361
- callback?: (content: T): boolean | Promise<boolean>;
362
- }
419
+ @RabbitConsumer({
420
+ queue: 'order.process',
421
+ exchangeName: 'orders',
422
+ routingKey: 'order.process',
423
+ retryStrategy: {
424
+ enabled: true,
425
+ maxAttempts: 5,
426
+ delay: (content, attempt, error) => attempt * 5000,
427
+ },
428
+ })
429
+ async processOrder(content: OrderPayload) { ... }
363
430
  ```
364
431
 
365
- By default the `suffix` for all DLQs will be `.dlq`.
432
+ The `delay` callback receives the message content, the current attempt number,
433
+ and the error that was thrown. It should return the delay in milliseconds
434
+ before the next retry. The return value controls the behavior:
366
435
 
367
- When giving a callback function, the library will execute it after finishing
368
- executing all retry attempts. This is useful if, in case of failure, you want
369
- to update your database, send an alert or anything else.
436
+ - A positive number: the message is sent to the retry queue with that TTL
437
+ - Zero: the message is retried immediately (republished to the end of the
438
+ original queue)
439
+ - A negative number: retrying is skipped entirely, and the message goes
440
+ straight to the dead letter strategy
370
441
 
371
- The function expects you to return a boolean, where depending on the result
372
- it will behave differently, such as:
442
+ **Defaults** (when `retryStrategy` is not specified):
373
443
 
374
- - When returning `TRUE`, the callback will be executed and the message will be
375
- forwarded to DLQ
376
- - When returning `FALSE`, **the message will not be sent to the DLQ, skipping
377
- it entirely**, allowing you to drop messages if the callback execution is
378
- successful
444
+ - `enabled`: true
445
+ - `maxAttempts`: 5
446
+ - `delay`: () => 5000
379
447
 
380
- Finally, if the callback throws any errors or is unable to be executed,
381
- an error message will be thrown with the reason and the message will be
382
- forwarded to the DLQ normally.
448
+ When the maximum number of attempts is reached, the message is nacked and sent
449
+ to the dead letter queue.
383
450
 
384
- ## Disabling the automatic ack
451
+ ## Dead Letter Strategy
385
452
 
386
- By default, the consumer will automatically send an ack at the end of the
387
- callback execution. If you need to disable this behaviour, you can pass:
453
+ Each consumer can define a `deadLetterStrategy` to control what happens when
454
+ a message exhausts all retry attempts:
388
455
 
389
456
  ```typescript
390
- consumerChannels: [
391
- options: {
392
- ...,
393
- autoAck: false
394
- }
395
- ]
457
+ @RabbitConsumer({
458
+ queue: 'order.process',
459
+ exchangeName: 'orders',
460
+ routingKey: 'order.process',
461
+ deadLetterStrategy: {
462
+ suffix: '.dlq',
463
+ callback: async (content) => {
464
+ await alertService.notify('Order processing failed', content);
465
+ return true;
466
+ },
467
+ },
468
+ })
469
+ async processOrder(content: OrderPayload) { ... }
396
470
  ```
397
471
 
398
- When disabled, it is necessary to manually acknowledge the message as follows:
472
+ The `suffix` controls the name of the dead letter queue. Defaults to `.dlq`,
473
+ resulting in a queue named `{queue}.dlq`.
474
+
475
+ The `callback` is executed before sending the message to the DLQ. It receives
476
+ the raw message content and should return a boolean:
477
+
478
+ - `true`: the message is forwarded to the DLQ after the callback executes
479
+ - `false`: the message is acknowledged and dropped, it will not go to the DLQ
480
+
481
+ If the callback throws an error, the message is forwarded to the DLQ regardless.
482
+
483
+ ## Disabling the automatic ack
484
+
485
+ By default, the consumer automatically acknowledges the message after the
486
+ handler completes. If you need manual control over acknowledgement, disable it:
399
487
 
400
488
  ```typescript
401
- async messageHandler(
402
- content: ChangeEventStatusUseCaseInput,
403
- params: RabbitConsumerParameters,
404
- ): Promise<void> {
405
- params.channel.ack(params.message);
489
+ @RabbitConsumer({
490
+ queue: 'order.process',
491
+ exchangeName: 'orders',
492
+ routingKey: 'order.process',
493
+ autoAck: false,
494
+ })
495
+ async processOrder(content: OrderPayload, params: MessageParams) {
496
+ // do work
497
+ params.channel.ack(params.message);
406
498
  }
407
499
  ```
408
500
 
409
- ## Custom Header metadata
501
+ When `autoAck` is disabled, you are responsible for calling `channel.ack()` or
502
+ `channel.nack()`. If you do not acknowledge the message, it will remain
503
+ unacknowledged and RabbitMQ will redeliver it when the consumer disconnects.
504
+
505
+ ## Custom Header Metadata
410
506
 
411
- Every published message contains the following custom header:
507
+ Every published message includes the following custom headers automatically:
412
508
 
413
509
  ```json
414
510
  {
415
- "x-application-headers": {
416
- "original-exchange": String,
417
- "original-routing-key": String,
418
- "published-at": ISODate
419
- }
511
+ "x-original-exchange": "exchange_name",
512
+ "x-original-routing-key": "routing.key",
513
+ "x-published-at": "2026-01-01T00:00:00.000Z"
420
514
  }
421
515
  ```
422
516
 
423
- This is important because when sending the message to the DLQ, the original
424
- routing-key and exchange references are lost.
517
+ These headers preserve the original exchange and routing key references, which
518
+ would otherwise be lost when a message is routed through retry queues or the
519
+ DLQ.
520
+
521
+ The `originalRoutingKey` field in `MessageParams` is derived from these headers
522
+ when available, falling back to the message's current routing key.
425
523
 
426
- ## Extra options
524
+ ## Extra Options
427
525
 
428
526
  ### Consumer manual loading
429
527
 
430
- This library attaches the consumers during the `OnApplicationBootstrap` lifecycle
431
- of the NestJS Application, meaning that the application will begin to receive
432
- messages as soon as the lifecycle is done.
528
+ Consumers are attached during the `OnApplicationBootstrap` lifecycle, which
529
+ means the application begins receiving messages as soon as all modules are
530
+ initialized, but before `app.listen()` resolves.
433
531
 
434
- If your application needs some time to initiate the consumers for some reason,
435
- (pods with limited resource for example), you can set the flag
436
- `extraOptions.consumerManualLoad: true` on the configuration file and manually
437
- call the consumer instantiation.
532
+ If you need consumers to start only after the HTTP server is ready (or need
533
+ to defer startup for any other reason), set `consumerManualLoad: true` and
534
+ call the initialization manually:
438
535
 
439
- **Example:**
536
+ ```typescript
537
+ RabbitMQModule.forRoot({
538
+ connectionString: 'amqp://localhost',
539
+ delayExchangeName: 'my_app',
540
+ assertExchanges: [],
541
+ extraOptions: {
542
+ consumerManualLoad: true,
543
+ },
544
+ })
545
+ ```
440
546
 
441
547
  ```typescript
442
548
  async function bootstrap() {
443
549
  const app = await NestFactory.create(AppModule);
444
550
  await app.listen(3000);
445
551
 
446
- const rabbit: RabbitMQService = app.get(RabbitMQService);
447
- await rabbit.createConsumers();
552
+ const rabbit = app.get(RabbitMQService);
553
+ await rabbit.startConsumers();
448
554
  }
449
555
  bootstrap();
450
556
  ```
451
557
 
558
+ You can also pass a group name to `startConsumers(group)` to initialize only consumers
559
+ belonging to that group.
560
+
452
561
  ### Message inspection and logging
453
562
 
454
- You can inspect the consumer/publisher messages by setting the parameter
455
- `extraOptions.logType` or setting the environment variable `RABBITMQ_LOG_TYPE`
456
- to either: `all | consumer | publisher | none`.
563
+ You can inspect consumer and publisher messages by setting `extraOptions.logType`
564
+ or the `RABBITMQ_LOG_TYPE` environment variable to one of:
457
565
 
458
- The default value is `none`
566
+ - `all` logs both consumer and publisher messages
567
+ - `consumer` — logs consumer messages only
568
+ - `publisher` — logs publisher messages only
569
+ - `none` — no message logging (default)
459
570
 
460
- You can also use the `extraOptions.loggerInstance` to pass your custom Logger
461
- as long as it follows the Logger/Console interfaces. The SDK will use the given
462
- instance to log any messages
571
+ The environment variable takes precedence over the config value.
463
572
 
464
- ## How to build this library locally?
573
+ Consumer errors are always logged regardless of this setting.
465
574
 
466
- Just pull the project and run:
575
+ ### Health check
467
576
 
468
- ```shell
469
- pnpm install
470
- pnpm build
471
- ```
577
+ You can check the connection status of both the consumer and publisher
578
+ connections:
472
579
 
473
- And should be good to go
580
+ ```typescript
581
+ const rabbit = app.get(RabbitMQService);
474
582
 
475
- ## Planned features
583
+ // Check all connections (returns 0 if any connection is offline)
584
+ const status = rabbit.checkHealth(); // 1 = online, 0 = offline
476
585
 
477
- - [x] Add tests
478
- - [ ] Improve semantics of the config file
479
- - [ ] Offer a retry mechanism without the `x-delay`
480
- - [ ] Make the publisher method strongly typed based on the `assertExchanges`
481
- `exchangeName` and `routingKeys` configurations
586
+ // Check a specific connection
587
+ const sharedStatus = rabbit.checkHealth('shared-bus');
588
+ ```
482
589
 
483
- ## Contribute
590
+ ## Building locally
484
591
 
485
- TBD
592
+ ```shell
593
+ pnpm install
594
+ pnpm build
595
+ ```
486
596
 
487
597
  ## License
488
598
 
489
- [MIT License](https://github.com/golevelup/nestjs/blob/master/LICENSE)
599
+ [MIT License](https://github.com/brunogaldino/nestjs-rabbitmq/blob/master/LICENSE)