@bgaldino/nestjs-rabbitmq 2.0.0-beta.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 (42) hide show
  1. package/README.md +432 -324
  2. package/dist/amqp-connection-manager.d.ts +15 -15
  3. package/dist/amqp-connection-manager.js +94 -141
  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 -3
  15. package/dist/index.js +6 -3
  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 -7
  27. package/dist/rabbitmq-service.js +45 -18
  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 +31 -9
  33. package/dist/rabbitmq.module.d.ts +4 -3
  34. package/dist/rabbitmq.module.js +12 -5
  35. package/dist/rabbitmq.module.js.map +1 -1
  36. package/dist/rabbitmq.types.d.ts +48 -38
  37. package/dist/rabbitmq.types.js.map +1 -1
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +1 -1
  40. package/dist/rabbitmq-consumers.d.ts +0 -19
  41. package/dist/rabbitmq-consumers.js +0 -218
  42. package/dist/rabbitmq-consumers.js.map +0 -1
package/README.md CHANGED
@@ -1,491 +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
- ## The readme is outdated until I finish the updated for the v2
38
-
39
- ## Description
40
-
41
- This module features an opinionated way of configuring the RabbitMQ connection
42
- by using a configuration file that describes the behaviour of all publishers
43
- and consumers present in your project
44
-
45
- ## Motivation
46
-
47
- I wanted to have a central place where its easier to open the project and see
48
- the declared behaviour of the RabbitMQ instance for that project, without having
49
- to look around for NestJS annotations, microservices or whatever. Simply to open,
50
- go to the configuration file and know exactly what I'm looking at and what I should
51
- expect.
52
-
53
- ### Requirements
54
-
55
- - @nestjs/common: ">9"
56
- - An RabbitMQ instance with the
57
- [RabbitMQ Delayed Message Plugin](https://github.com/rabbitmq/rabbitmq-delayed-message-exchange) installed
58
-
59
- ### Instalation
60
-
61
- ### 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
62
37
 
63
38
  ```shell
64
39
  pnpm add @bgaldino/nestjs-rabbitmq
65
40
  ```
66
41
 
67
- **YARN**
68
-
69
42
  ```shell
70
43
  yarn add @bgaldino/nestjs-rabbitmq
71
44
  ```
72
45
 
73
- **NPM**
74
-
75
46
  ```shell
76
47
  npm add @bgaldino/nestjs-rabbitmq
77
48
  ```
78
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
+
79
58
  ## Getting Started
80
59
 
81
- ### 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
82
64
 
83
- Import the `RabbitMQModule` in your `app.module.ts` and call the method `register({})`
65
+ For simple setups where the configuration is static:
84
66
 
85
67
  ```typescript
86
68
  import { RabbitMQModule } from '@bgaldino/nestjs-rabbitmq';
87
- import { RabbitOptions } from '@bgaldino/nestjs-rabbitmq';
88
69
 
89
70
  @Module({
90
71
  imports: [
91
- ...
92
- RabbitMQModule.register({ useClass: RabbitOptions, injects: [...] }),
93
- ...
94
- ]
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
+ ],
95
81
  })
96
82
  export class AppModule {}
97
83
  ```
98
84
 
99
- The `RabbitMQModule` is marked as `@Global`, therefore, calling it once is enough
100
- to allow the injection of the `RabbitMQService`
85
+ ### forRootAsync
101
86
 
102
- ### The configuration file
103
-
104
- Create a `rabbitmq.config.ts` or whatever the name you prefer containing the
105
- minimum configuration:
87
+ When you need to inject dependencies or resolve configuration asynchronously:
106
88
 
107
89
  ```typescript
108
- import { Injectable } from "@nestjs/common";
109
- import {
110
- RabbitMQModuleOptions,
111
- RabbitOptionsFactory,
112
- } from "@bgaldino/nestjs-rabbitmq";
90
+ import { RabbitMQModule, RabbitMQOptionsFactory, ModuleOptions } from '@bgaldino/nestjs-rabbitmq';
113
91
 
114
92
  @Injectable()
115
- export class RabbitOptions implements RabbitOptionsFactory {
116
- createRabbitOptions(): RabbitMQModuleOptions {
93
+ class RabbitConfig implements RabbitMQOptionsFactory {
94
+ constructor(private readonly configService: ConfigService) {}
95
+
96
+ createRabbitOptions(): ModuleOptions {
117
97
  return {
118
- connectionString: "amqp://{user}:{password}@{url}/{vhost}",
119
- delayExchangeName: "MyDelayExchange",
120
- assertExchanges: [],
121
- consumerChannels: [],
98
+ connectionString: this.configService.get('RABBIT_URL'),
99
+ delayExchangeName: 'my_app',
100
+ assertExchanges: [
101
+ { name: 'orders', type: 'topic' },
102
+ ],
122
103
  };
123
104
  }
124
105
  }
125
- ```
126
106
 
127
- ## Publishers
107
+ @Module({
108
+ imports: [
109
+ RabbitMQModule.forRootAsync({
110
+ useClass: RabbitConfig,
111
+ imports: [ConfigModule],
112
+ }),
113
+ ],
114
+ })
115
+ export class AppModule {}
116
+ ```
128
117
 
129
- **Example config file**:
118
+ You can also use `useFactory` directly:
130
119
 
131
120
  ```typescript
132
- assertExchanges: [
133
- {
134
- name: 'webhooks', type: 'topic',
135
- options: { durable: true, autoDelete: false }
136
- },
137
- { name: 'test-fanout', type: 'fanout' },
138
- { name: 'example-direct', type: 'direct'},
139
- ],
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
+ })
140
130
  ```
141
131
 
142
- The `assertExchanges` property expects an array of `RabbitMQAssertExchange`
143
- and each entry will asserted against the RabbitMQ connected server.
132
+ ## Consumers
144
133
 
145
- If any entry does not match a current configuration, or cannot be
146
- created/attached. A terminal error `406 - PRECONDITION_FAILED` will be thrown
147
- 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.
148
137
 
149
- ### 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`).
150
141
 
151
- **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.
152
148
 
153
149
  ```typescript
154
- import { Injectable } from "@nestjs/common";
155
- import { RabbitMQService } from "@bgaldino/nestjs-rabbitmq";
150
+ import { Injectable } from '@nestjs/common';
151
+ import { RabbitConsumer, ConsumerOptions, MessageParams } from '@bgaldino/nestjs-rabbitmq';
156
152
 
157
153
  @Injectable()
158
- export class MyService {
159
- constructor(private readonly rabbitMQService: RabbitMQService) {}
160
- }
161
-
162
- async publishMe(){
163
- const isPublished = await this.rabbitMQService.publish('exchange_name', 'routing_key', {});
164
- }
165
-
166
- //or
167
-
168
- async publishMeTyped() {
169
- const isPublished = await this.rabbitMQService.publish<CustomType>('exchange_name', 'routing_key', {});
170
- //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
+ }
171
164
  }
172
165
  ```
173
166
 
174
- The `publish()` method uses [Publish Confirms](https://www.rabbitmq.com/docs/confirms#publisher-confirms)
175
- to make sure that the message is delivered to the broker before returning
176
- the promise.
167
+ The provider must be registered in a NestJS module. The library scans all
168
+ providers and controllers for decorated methods.
177
169
 
178
- ### Custom headers
170
+ ### Config-based consumers
179
171
 
180
- The library defines a couple custom headers
181
-
182
- ## Consumers
183
-
184
- Inside the configuration file you can declare your consumers on the section
185
- `consumerChannels`. This list of `RabbitMQConsumerChannel` will be evaluated
186
- and each entry will try to create the queue and bind it to the declared
187
- exchange.
188
-
189
- **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:
190
175
 
191
176
  ```typescript
192
-
193
- createRabbitOptions(): RabbitMQModuleOptions {
194
- return {
195
- ...,
196
- consumerChannels: [
197
- {
198
- options: {
199
- queue: 'myqueue',
200
- exchangeName: 'foobar.exchange',
201
- routingKey: 'myqueue',
202
- prefetch: Number(process.env.RABBIT_PREFETCH ?? 10),
203
- retryStrategy: {
204
- enabled: true,
205
- maxAttempts: 5,
206
- delay: (attempt: number) => {
207
- return attempt * 5000;
208
- },
209
- },
210
- },
211
- messageHandler: this.consumerService.messageHandler.bind(
212
- this.consumerService,
213
- ),
214
- },
215
- ]
216
- }
217
- }
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
+ })
218
196
  ```
219
197
 
220
- The consumer **DOES NOT** create exchanges and only bind to ones that already
221
- exists. This is to avoid creating exchanges with typos and
222
- misconfigurations.
198
+ The library resolves the provider instance automatically. No need to manually
199
+ bind `this` or inject the service into your config.
223
200
 
224
- You can also declare an array of `routingKeys: string[]` if you want to attach
225
- multiple keys to the same queue/callback
201
+ ### Mixed usage
226
202
 
227
- ### The messageHandler callback
203
+ Both approaches can coexist. A common pattern is using decorators for static
204
+ consumers and config for conditional ones:
228
205
 
229
- As declared in the example above, the `messageHandler` property expects a
230
- callback of the type `IRabbitHandler`. Because of the nature of the library,
231
- we will need to call the `.bind(this.yourService)` in order to bind the `this`
232
- 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
+ ```
233
216
 
234
- The `RabbitMQModule.register()` accepts an array of NestJS Modules with the
235
- any module that contains an consumer callback function.
217
+ ### Handler signature
236
218
 
237
- The `callback` has the following signature:
219
+ Every consumer handler follows the same signature regardless of registration
220
+ method:
238
221
 
239
222
  ```typescript
240
- async messageHandler(content: any, params?: RabbitConsumerParams): Promise<void>;
223
+ async handler(content: T, params?: MessageParams): Promise<void>;
241
224
  ```
242
225
 
243
- where `RabbitConsumerParams` is optional and contains the following info:
226
+ The `MessageParams` object is optional and contains:
244
227
 
245
228
  ```typescript
246
- export type RabbitConsumerParameters = {
229
+ type MessageParams = {
247
230
  message: ConsumeMessage;
248
231
  channel: ConfirmChannel;
249
232
  queue: string;
233
+ originalRoutingKey?: string;
250
234
  };
251
235
  ```
252
236
 
253
- ### 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.
254
240
 
255
- 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:
256
243
 
257
244
  ```typescript
258
- export interface MyInterface {
259
- type: string;
260
- id: number;
261
- }
245
+ import { ConsumerHandler, MessageParams } from '@bgaldino/nestjs-rabbitmq';
262
246
 
263
247
  @Injectable()
264
- export class MyClass implements IRabbitConsumer<MyInterface> {
265
- public async messageHandler(content: MyInterface): Promise<void> {
266
- 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);
267
251
  }
268
252
  }
269
253
  ```
270
254
 
271
- ### Declaration example
255
+ ### Consumer groups
272
256
 
273
- **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.
274
260
 
275
261
  ```typescript
276
- //consumer.module.ts
277
- @Module({
278
- provides: [ConsumerService],
279
- exports: [ConsumerService],
262
+ @RabbitConsumer({
263
+ queue: 'heavy.processing',
264
+ exchangeName: 'jobs',
265
+ routingKey: 'heavy.*',
266
+ group: 'workers',
280
267
  })
281
- 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';
282
290
 
283
- //consumer.service.ts
284
291
  @Injectable()
285
- export class ConsumerService {
286
- async messageHandler(content: any) {
287
- 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
+ }
288
301
  }
289
302
  }
290
303
  ```
291
304
 
292
- **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:
293
312
 
294
313
  ```typescript
295
- //rabbit.config.ts
296
- @Injectable()
297
- export class RabbitOptions implements RabbitOptionsFactory {
298
- constructor(
299
- readonly consumerService: ConsumerService ,
300
- ) {}
301
-
302
- createRabbitOptions(): RabbitMQModuleOptions {
303
- return {
304
- ...
305
- consumerchannels: [
306
- {
307
- options: {
308
- queue: "myqueue",
309
- exchangename: "test.exchange",
310
- routingkey: "myqueue",
311
- prefetch: number(process.env.rabbit_prefetch ?? 10),
312
- },
313
- messagehandler: this.MyService.messageHandler.bind(this.MyService),
314
- },
315
- ];
316
- }
317
- }
314
+ await this.rabbit.publish<OrderPayload>('orders', 'order.created', {
315
+ orderId: '123',
316
+ amount: 99.90,
317
+ });
318
+ ```
318
319
 
319
- //app.module.ts
320
- @Module({
321
- imports: [
322
- ...,
323
- RabbitMQModule.register({
324
- useClass: RabbitConfig,
325
- injects: [ConsumerModule]
326
- })
327
- ]
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
+ ],
328
353
  })
329
- export class AppModule {}
330
354
  ```
331
355
 
332
- ## 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
333
362
 
334
- On each consumer declaration you can pass the optional parameter: `retryStrategy`
335
- 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"`:
336
366
 
337
367
  ```typescript
338
- retryStrategy: {
339
- enabled?: true,
340
- maxAttempts?: 5,
341
- delay?: (attempt) => {return attempt * 5000};
342
- }
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
+ ],
343
397
  ```
344
398
 
345
- By default, the `retryStrategy` is enabled. When consuming a new message and
346
- the callback throws an error.
399
+ ### Publishing to a specific connection
400
+
401
+ Pass the `connection` option to `publish()`:
347
402
 
348
- When enabled, the library will create a `{options.queue}.dlq` queue and will use
349
- the `delayExchangeName` exchange as the retrying orchestrator where the
350
- `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
+ ```
351
408
 
352
- When the maximum attempts is reached, the library issues a nack, sending the
353
- message to the `.dlq` queue.
409
+ When omitted, the message is published to the `"default"` connection.
354
410
 
355
- ## Deadletter strategy
411
+ ## Retry Strategy
356
412
 
357
- Each consumer can have an optional parameter `deadletterStrategy` with the
358
- 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.
359
417
 
360
418
  ```typescript
361
- retryStrategy: {
362
- suffix: string
363
- callback?: (content: T): boolean | Promise<boolean>;
364
- }
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) { ... }
365
430
  ```
366
431
 
367
- 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:
368
435
 
369
- When giving a callback function, the library will execute it after finishing
370
- executing all retry attempts. This is useful if, in case of failure, you want
371
- 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
372
441
 
373
- The function expects you to return a boolean, where depending on the result
374
- it will behave differently, such as:
442
+ **Defaults** (when `retryStrategy` is not specified):
375
443
 
376
- - When returning `TRUE`, the callback will be executed and the message will be
377
- forwarded to DLQ
378
- - When returning `FALSE`, **the message will not be sent to the DLQ, skipping
379
- it entirely**, allowing you to drop messages if the callback execution is
380
- successful
444
+ - `enabled`: true
445
+ - `maxAttempts`: 5
446
+ - `delay`: () => 5000
381
447
 
382
- Finally, if the callback throws any errors or is unable to be executed,
383
- an error message will be thrown with the reason and the message will be
384
- 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.
385
450
 
386
- ## Disabling the automatic ack
451
+ ## Dead Letter Strategy
387
452
 
388
- By default, the consumer will automatically send an ack at the end of the
389
- 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:
390
455
 
391
456
  ```typescript
392
- consumerChannels: [
393
- options: {
394
- ...,
395
- autoAck: false
396
- }
397
- ]
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) { ... }
398
470
  ```
399
471
 
400
- 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:
401
487
 
402
488
  ```typescript
403
- async messageHandler(
404
- content: ChangeEventStatusUseCaseInput,
405
- params: RabbitConsumerParameters,
406
- ): Promise<void> {
407
- 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);
408
498
  }
409
499
  ```
410
500
 
411
- ## 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
412
506
 
413
- Every published message contains the following custom header:
507
+ Every published message includes the following custom headers automatically:
414
508
 
415
509
  ```json
416
510
  {
417
- "x-application-headers": {
418
- "original-exchange": String,
419
- "original-routing-key": String,
420
- "published-at": ISODate
421
- }
511
+ "x-original-exchange": "exchange_name",
512
+ "x-original-routing-key": "routing.key",
513
+ "x-published-at": "2026-01-01T00:00:00.000Z"
422
514
  }
423
515
  ```
424
516
 
425
- This is important because when sending the message to the DLQ, the original
426
- 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.
427
523
 
428
- ## Extra options
524
+ ## Extra Options
429
525
 
430
526
  ### Consumer manual loading
431
527
 
432
- This library attaches the consumers during the `OnApplicationBootstrap` lifecycle
433
- of the NestJS Application, meaning that the application will begin to receive
434
- 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.
435
531
 
436
- If your application needs some time to initiate the consumers for some reason,
437
- (pods with limited resource for example), you can set the flag
438
- `extraOptions.consumerManualLoad: true` on the configuration file and manually
439
- 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:
440
535
 
441
- **Example:**
536
+ ```typescript
537
+ RabbitMQModule.forRoot({
538
+ connectionString: 'amqp://localhost',
539
+ delayExchangeName: 'my_app',
540
+ assertExchanges: [],
541
+ extraOptions: {
542
+ consumerManualLoad: true,
543
+ },
544
+ })
545
+ ```
442
546
 
443
547
  ```typescript
444
548
  async function bootstrap() {
445
549
  const app = await NestFactory.create(AppModule);
446
550
  await app.listen(3000);
447
551
 
448
- const rabbit: AMQPConnectionManager = app.get(AMQPConnectionManager);
449
- await rabbit.createConsumers();
552
+ const rabbit = app.get(RabbitMQService);
553
+ await rabbit.startConsumers();
450
554
  }
451
555
  bootstrap();
452
556
  ```
453
557
 
558
+ You can also pass a group name to `startConsumers(group)` to initialize only consumers
559
+ belonging to that group.
560
+
454
561
  ### Message inspection and logging
455
562
 
456
- You can inspect the consumer/publisher messages by setting the parameter
457
- `extraOptions.logType` or setting the environment variable `RABBITMQ_LOG_TYPE`
458
- 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:
459
565
 
460
- 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)
461
570
 
462
- You can also use the `extraOptions.loggerInstance` to pass your custom Logger
463
- as long as it follows the Logger/Console interfaces. The SDK will use the given
464
- instance to log any messages
571
+ The environment variable takes precedence over the config value.
465
572
 
466
- ## How to build this library locally?
573
+ Consumer errors are always logged regardless of this setting.
467
574
 
468
- Just pull the project and run:
575
+ ### Health check
469
576
 
470
- ```shell
471
- pnpm install
472
- pnpm build
473
- ```
577
+ You can check the connection status of both the consumer and publisher
578
+ connections:
474
579
 
475
- And should be good to go
580
+ ```typescript
581
+ const rabbit = app.get(RabbitMQService);
476
582
 
477
- ## Planned features
583
+ // Check all connections (returns 0 if any connection is offline)
584
+ const status = rabbit.checkHealth(); // 1 = online, 0 = offline
478
585
 
479
- - [x] Add tests
480
- - [ ] Improve semantics of the config file
481
- - [ ] Offer a retry mechanism without the `x-delay`
482
- - [ ] Make the publisher method strongly typed based on the `assertExchanges`
483
- `exchangeName` and `routingKeys` configurations
586
+ // Check a specific connection
587
+ const sharedStatus = rabbit.checkHealth('shared-bus');
588
+ ```
484
589
 
485
- ## Contribute
590
+ ## Building locally
486
591
 
487
- TBD
592
+ ```shell
593
+ pnpm install
594
+ pnpm build
595
+ ```
488
596
 
489
597
  ## License
490
598
 
491
- [MIT License](https://github.com/golevelup/nestjs/blob/master/LICENSE)
599
+ [MIT License](https://github.com/brunogaldino/nestjs-rabbitmq/blob/master/LICENSE)