@bgaldino/nestjs-rabbitmq 2.0.0-beta.1 → 2.0.0-beta.4
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 +446 -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 +14 -0
- package/dist/class-discovery.js +97 -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 +64 -42
- 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,613 @@
|
|
|
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
|
-
|
|
132
|
+
## Consumers
|
|
133
|
+
|
|
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.
|
|
144
137
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
###
|
|
142
|
+
### Decorator-based consumers
|
|
150
143
|
|
|
151
|
-
|
|
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.
|
|
177
|
-
|
|
178
|
-
### Custom headers
|
|
167
|
+
The provider must be registered in a NestJS module. The library scans all
|
|
168
|
+
providers and controllers for decorated methods.
|
|
179
169
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
## Consumers
|
|
170
|
+
### Config-based consumers
|
|
183
171
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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) { ... }
|
|
343
375
|
```
|
|
344
376
|
|
|
345
|
-
|
|
346
|
-
|
|
377
|
+
Config-based consumers declared inside a connection's `consumerChannels` are
|
|
378
|
+
automatically scoped to that connection:
|
|
347
379
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
],
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Publishing to a specific connection
|
|
400
|
+
|
|
401
|
+
Pass the `connection` option to `publish()`:
|
|
402
|
+
|
|
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
|
+
You can also give a "string" value referring a method of the same class,
|
|
449
|
+
following the interface:
|
|
385
450
|
|
|
386
|
-
|
|
451
|
+
```typescript
|
|
452
|
+
function retry(content: any, attempt: number, exception: Error): Promise<number> | number;
|
|
453
|
+
```
|
|
387
454
|
|
|
388
|
-
|
|
389
|
-
|
|
455
|
+
When the maximum number of attempts is reached, the message is nacked and sent
|
|
456
|
+
to the dead letter queue.
|
|
457
|
+
|
|
458
|
+
## Dead Letter Strategy
|
|
459
|
+
|
|
460
|
+
Each consumer can define a `deadLetterStrategy` to control what happens when
|
|
461
|
+
a message exhausts all retry attempts:
|
|
390
462
|
|
|
391
463
|
```typescript
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
464
|
+
@RabbitConsumer({
|
|
465
|
+
queue: 'order.process',
|
|
466
|
+
exchangeName: 'orders',
|
|
467
|
+
routingKey: 'order.process',
|
|
468
|
+
deadLetterStrategy: {
|
|
469
|
+
suffix: '.dlq',
|
|
470
|
+
callback: async (content) => {
|
|
471
|
+
await alertService.notify('Order processing failed', content);
|
|
472
|
+
return true;
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
async processOrder(content: OrderPayload) { ... }
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The `suffix` controls the name of the dead letter queue. Defaults to `.dlq`,
|
|
480
|
+
resulting in a queue named `{queue}.dlq`.
|
|
481
|
+
|
|
482
|
+
The `callback` is executed before sending the message to the DLQ. It receives
|
|
483
|
+
the raw message content and should return a boolean:
|
|
484
|
+
|
|
485
|
+
- `true`: the message is forwarded to the DLQ after the callback executes
|
|
486
|
+
- `false`: the message is acknowledged and dropped, it will not go to the DLQ
|
|
487
|
+
|
|
488
|
+
If the callback throws an error, the message is forwarded to the DLQ regardless.
|
|
489
|
+
|
|
490
|
+
Like the `retryStrategy`, you can pass a "string" of the method name you want
|
|
491
|
+
to call if it is in the same class. The method should implement the interface:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
function (content: T): Promise<boolean> | boolean;
|
|
398
495
|
```
|
|
399
496
|
|
|
400
|
-
|
|
497
|
+
## Disabling the automatic ack
|
|
498
|
+
|
|
499
|
+
By default, the consumer automatically acknowledges the message after the
|
|
500
|
+
handler completes. If you need manual control over acknowledgement, disable it:
|
|
401
501
|
|
|
402
502
|
```typescript
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
503
|
+
@RabbitConsumer({
|
|
504
|
+
queue: 'order.process',
|
|
505
|
+
exchangeName: 'orders',
|
|
506
|
+
routingKey: 'order.process',
|
|
507
|
+
autoAck: false,
|
|
508
|
+
})
|
|
509
|
+
async processOrder(content: OrderPayload, params: MessageParams) {
|
|
510
|
+
// do work
|
|
511
|
+
params.channel.ack(params.message);
|
|
408
512
|
}
|
|
409
513
|
```
|
|
410
514
|
|
|
411
|
-
|
|
515
|
+
When `autoAck` is disabled, you are responsible for calling `channel.ack()` or
|
|
516
|
+
`channel.nack()`. If you do not acknowledge the message, it will remain
|
|
517
|
+
unacknowledged and RabbitMQ will redeliver it when the consumer disconnects.
|
|
518
|
+
|
|
519
|
+
## Custom Header Metadata
|
|
412
520
|
|
|
413
|
-
Every published message
|
|
521
|
+
Every published message includes the following custom headers automatically:
|
|
414
522
|
|
|
415
523
|
```json
|
|
416
524
|
{
|
|
417
|
-
"x-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"published-at": ISODate
|
|
421
|
-
}
|
|
525
|
+
"x-original-exchange": "exchange_name",
|
|
526
|
+
"x-original-routing-key": "routing.key",
|
|
527
|
+
"x-published-at": "2026-01-01T00:00:00.000Z"
|
|
422
528
|
}
|
|
423
529
|
```
|
|
424
530
|
|
|
425
|
-
|
|
426
|
-
|
|
531
|
+
These headers preserve the original exchange and routing key references, which
|
|
532
|
+
would otherwise be lost when a message is routed through retry queues or the
|
|
533
|
+
DLQ.
|
|
534
|
+
|
|
535
|
+
The `originalRoutingKey` field in `MessageParams` is derived from these headers
|
|
536
|
+
when available, falling back to the message's current routing key.
|
|
427
537
|
|
|
428
|
-
## Extra
|
|
538
|
+
## Extra Options
|
|
429
539
|
|
|
430
540
|
### Consumer manual loading
|
|
431
541
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
542
|
+
Consumers are attached during the `OnApplicationBootstrap` lifecycle, which
|
|
543
|
+
means the application begins receiving messages as soon as all modules are
|
|
544
|
+
initialized, but before `app.listen()` resolves.
|
|
435
545
|
|
|
436
|
-
If
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
call the consumer instantiation.
|
|
546
|
+
If you need consumers to start only after the HTTP server is ready (or need
|
|
547
|
+
to defer startup for any other reason), set `consumerManualLoad: true` and
|
|
548
|
+
call the initialization manually:
|
|
440
549
|
|
|
441
|
-
|
|
550
|
+
```typescript
|
|
551
|
+
RabbitMQModule.forRoot({
|
|
552
|
+
connectionString: 'amqp://localhost',
|
|
553
|
+
delayExchangeName: 'my_app',
|
|
554
|
+
assertExchanges: [],
|
|
555
|
+
extraOptions: {
|
|
556
|
+
consumerManualLoad: true,
|
|
557
|
+
},
|
|
558
|
+
})
|
|
559
|
+
```
|
|
442
560
|
|
|
443
561
|
```typescript
|
|
444
562
|
async function bootstrap() {
|
|
445
563
|
const app = await NestFactory.create(AppModule);
|
|
446
564
|
await app.listen(3000);
|
|
447
565
|
|
|
448
|
-
const rabbit
|
|
449
|
-
await rabbit.
|
|
566
|
+
const rabbit = app.get(RabbitMQService);
|
|
567
|
+
await rabbit.startConsumers();
|
|
450
568
|
}
|
|
451
569
|
bootstrap();
|
|
452
570
|
```
|
|
453
571
|
|
|
572
|
+
You can also pass a group name to `startConsumers(group)` to initialize only consumers
|
|
573
|
+
belonging to that group.
|
|
574
|
+
|
|
454
575
|
### Message inspection and logging
|
|
455
576
|
|
|
456
|
-
You can inspect
|
|
457
|
-
|
|
458
|
-
to either: `all | consumer | publisher | none`.
|
|
577
|
+
You can inspect consumer and publisher messages by setting `extraOptions.logType`
|
|
578
|
+
or the `RABBITMQ_LOG_TYPE` environment variable to one of:
|
|
459
579
|
|
|
460
|
-
|
|
580
|
+
- `all` — logs both consumer and publisher messages
|
|
581
|
+
- `consumer` — logs consumer messages only
|
|
582
|
+
- `publisher` — logs publisher messages only
|
|
583
|
+
- `none` — no message logging (default)
|
|
461
584
|
|
|
462
|
-
|
|
463
|
-
as long as it follows the Logger/Console interfaces. The SDK will use the given
|
|
464
|
-
instance to log any messages
|
|
585
|
+
The environment variable takes precedence over the config value.
|
|
465
586
|
|
|
466
|
-
|
|
587
|
+
Consumer errors are always logged regardless of this setting.
|
|
467
588
|
|
|
468
|
-
|
|
589
|
+
### Health check
|
|
469
590
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
pnpm build
|
|
473
|
-
```
|
|
591
|
+
You can check the connection status of both the consumer and publisher
|
|
592
|
+
connections:
|
|
474
593
|
|
|
475
|
-
|
|
594
|
+
```typescript
|
|
595
|
+
const rabbit = app.get(RabbitMQService);
|
|
476
596
|
|
|
477
|
-
|
|
597
|
+
// Check all connections (returns 0 if any connection is offline)
|
|
598
|
+
const status = rabbit.checkHealth(); // 1 = online, 0 = offline
|
|
478
599
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
- [ ] Make the publisher method strongly typed based on the `assertExchanges`
|
|
483
|
-
`exchangeName` and `routingKeys` configurations
|
|
600
|
+
// Check a specific connection
|
|
601
|
+
const sharedStatus = rabbit.checkHealth('shared-bus');
|
|
602
|
+
```
|
|
484
603
|
|
|
485
|
-
##
|
|
604
|
+
## Building locally
|
|
486
605
|
|
|
487
|
-
|
|
606
|
+
```shell
|
|
607
|
+
pnpm install
|
|
608
|
+
pnpm build
|
|
609
|
+
```
|
|
488
610
|
|
|
489
611
|
## License
|
|
490
612
|
|
|
491
|
-
[MIT License](https://github.com/
|
|
613
|
+
[MIT License](https://github.com/brunogaldino/nestjs-rabbitmq/blob/master/LICENSE)
|