@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.
- package/README.md +432 -324
- package/dist/amqp-connection-manager.d.ts +15 -15
- package/dist/amqp-connection-manager.js +94 -141
- package/dist/amqp-connection-manager.js.map +1 -1
- package/dist/class-discovery.d.ts +13 -0
- package/dist/class-discovery.js +78 -0
- package/dist/class-discovery.js.map +1 -0
- package/dist/connection-factory.d.ts +20 -0
- package/dist/connection-factory.js +106 -0
- package/dist/connection-factory.js.map +1 -0
- package/dist/helper.d.ts +1 -1
- package/dist/helper.js +3 -7
- package/dist/helper.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/rabbit-consumer.decorator.d.ts +7 -0
- package/dist/rabbit-consumer.decorator.js +13 -0
- package/dist/rabbit-consumer.decorator.js.map +1 -0
- package/dist/rabbitmq-consumer.d.ts +19 -0
- package/dist/rabbitmq-consumer.js +183 -0
- package/dist/rabbitmq-consumer.js.map +1 -0
- package/dist/rabbitmq-retry-handler.d.ts +10 -0
- package/dist/rabbitmq-retry-handler.js +46 -0
- package/dist/rabbitmq-retry-handler.js.map +1 -0
- package/dist/rabbitmq-service.d.ts +12 -7
- package/dist/rabbitmq-service.js +45 -18
- package/dist/rabbitmq-service.js.map +1 -1
- package/dist/rabbitmq.constants.d.ts +1 -0
- package/dist/rabbitmq.constants.js +5 -0
- package/dist/rabbitmq.constants.js.map +1 -0
- package/dist/rabbitmq.interfaces.d.ts +31 -9
- package/dist/rabbitmq.module.d.ts +4 -3
- package/dist/rabbitmq.module.js +12 -5
- package/dist/rabbitmq.module.js.map +1 -1
- package/dist/rabbitmq.types.d.ts +48 -38
- package/dist/rabbitmq.types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/rabbitmq-consumers.d.ts +0 -19
- package/dist/rabbitmq-consumers.js +0 -218
- package/dist/rabbitmq-consumers.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,491 +1,599 @@
|
|
|
1
1
|
# @bgaldino/nestjs-rabbitmq
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
|
|
10
|
-
- [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
to allow the injection of the `RabbitMQService`
|
|
85
|
+
### forRootAsync
|
|
101
86
|
|
|
102
|
-
|
|
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 {
|
|
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
|
-
|
|
116
|
-
|
|
93
|
+
class RabbitConfig implements RabbitMQOptionsFactory {
|
|
94
|
+
constructor(private readonly configService: ConfigService) {}
|
|
95
|
+
|
|
96
|
+
createRabbitOptions(): ModuleOptions {
|
|
117
97
|
return {
|
|
118
|
-
connectionString:
|
|
119
|
-
delayExchangeName:
|
|
120
|
-
assertExchanges: [
|
|
121
|
-
|
|
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
|
-
|
|
107
|
+
@Module({
|
|
108
|
+
imports: [
|
|
109
|
+
RabbitMQModule.forRootAsync({
|
|
110
|
+
useClass: RabbitConfig,
|
|
111
|
+
imports: [ConfigModule],
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
114
|
+
})
|
|
115
|
+
export class AppModule {}
|
|
116
|
+
```
|
|
128
117
|
|
|
129
|
-
|
|
118
|
+
You can also use `useFactory` directly:
|
|
130
119
|
|
|
131
120
|
```typescript
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
143
|
-
and each entry will asserted against the RabbitMQ connected server.
|
|
132
|
+
## Consumers
|
|
144
133
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
import {
|
|
150
|
+
import { Injectable } from '@nestjs/common';
|
|
151
|
+
import { RabbitConsumer, ConsumerOptions, MessageParams } from '@bgaldino/nestjs-rabbitmq';
|
|
156
152
|
|
|
157
153
|
@Injectable()
|
|
158
|
-
export class
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
//
|
|
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
|
|
175
|
-
|
|
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
|
-
###
|
|
170
|
+
### Config-based consumers
|
|
179
171
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
multiple keys to the same queue/callback
|
|
201
|
+
### Mixed usage
|
|
226
202
|
|
|
227
|
-
|
|
203
|
+
Both approaches can coexist. A common pattern is using decorators for static
|
|
204
|
+
consumers and config for conditional ones:
|
|
228
205
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
any module that contains an consumer callback function.
|
|
217
|
+
### Handler signature
|
|
236
218
|
|
|
237
|
-
|
|
219
|
+
Every consumer handler follows the same signature regardless of registration
|
|
220
|
+
method:
|
|
238
221
|
|
|
239
222
|
```typescript
|
|
240
|
-
async
|
|
223
|
+
async handler(content: T, params?: MessageParams): Promise<void>;
|
|
241
224
|
```
|
|
242
225
|
|
|
243
|
-
|
|
226
|
+
The `MessageParams` object is optional and contains:
|
|
244
227
|
|
|
245
228
|
```typescript
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
241
|
+
You can implement the `ConsumerHandler<T>` interface to strongly type your
|
|
242
|
+
consumer:
|
|
256
243
|
|
|
257
244
|
```typescript
|
|
258
|
-
|
|
259
|
-
type: string;
|
|
260
|
-
id: number;
|
|
261
|
-
}
|
|
245
|
+
import { ConsumerHandler, MessageParams } from '@bgaldino/nestjs-rabbitmq';
|
|
262
246
|
|
|
263
247
|
@Injectable()
|
|
264
|
-
export class
|
|
265
|
-
|
|
266
|
-
console.log(content.
|
|
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
|
-
###
|
|
255
|
+
### Consumer groups
|
|
272
256
|
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
262
|
+
@RabbitConsumer({
|
|
263
|
+
queue: 'heavy.processing',
|
|
264
|
+
exchangeName: 'jobs',
|
|
265
|
+
routingKey: 'heavy.*',
|
|
266
|
+
group: 'workers',
|
|
280
267
|
})
|
|
281
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
|
|
399
|
+
### Publishing to a specific connection
|
|
400
|
+
|
|
401
|
+
Pass the `connection` option to `publish()`:
|
|
347
402
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
403
|
+
```typescript
|
|
404
|
+
await this.rabbit.publish('events', 'audit.created', payload, {
|
|
405
|
+
connection: 'shared-bus',
|
|
406
|
+
});
|
|
407
|
+
```
|
|
351
408
|
|
|
352
|
-
When the
|
|
353
|
-
message to the `.dlq` queue.
|
|
409
|
+
When omitted, the message is published to the `"default"` connection.
|
|
354
410
|
|
|
355
|
-
##
|
|
411
|
+
## Retry Strategy
|
|
356
412
|
|
|
357
|
-
Each consumer can
|
|
358
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
374
|
-
it will behave differently, such as:
|
|
442
|
+
**Defaults** (when `retryStrategy` is not specified):
|
|
375
443
|
|
|
376
|
-
-
|
|
377
|
-
|
|
378
|
-
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
##
|
|
451
|
+
## Dead Letter Strategy
|
|
387
452
|
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
|
507
|
+
Every published message includes the following custom headers automatically:
|
|
414
508
|
|
|
415
509
|
```json
|
|
416
510
|
{
|
|
417
|
-
"x-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
|
524
|
+
## Extra Options
|
|
429
525
|
|
|
430
526
|
### Consumer manual loading
|
|
431
527
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
|
449
|
-
await rabbit.
|
|
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
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
573
|
+
Consumer errors are always logged regardless of this setting.
|
|
467
574
|
|
|
468
|
-
|
|
575
|
+
### Health check
|
|
469
576
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
pnpm build
|
|
473
|
-
```
|
|
577
|
+
You can check the connection status of both the consumer and publisher
|
|
578
|
+
connections:
|
|
474
579
|
|
|
475
|
-
|
|
580
|
+
```typescript
|
|
581
|
+
const rabbit = app.get(RabbitMQService);
|
|
476
582
|
|
|
477
|
-
|
|
583
|
+
// Check all connections (returns 0 if any connection is offline)
|
|
584
|
+
const status = rabbit.checkHealth(); // 1 = online, 0 = offline
|
|
478
585
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
##
|
|
590
|
+
## Building locally
|
|
486
591
|
|
|
487
|
-
|
|
592
|
+
```shell
|
|
593
|
+
pnpm install
|
|
594
|
+
pnpm build
|
|
595
|
+
```
|
|
488
596
|
|
|
489
597
|
## License
|
|
490
598
|
|
|
491
|
-
[MIT License](https://github.com/
|
|
599
|
+
[MIT License](https://github.com/brunogaldino/nestjs-rabbitmq/blob/master/LICENSE)
|