Queued notifications
Move delivery off the request, end to end — set shouldQueue, register the notification and resolveNotifiable, and pick a dispatcher. The call site never changes.
Delivering over SMTP or to a third-party API on the request thread is slow and fragile. Queue it
instead: the request enqueues a job and returns immediately, and a worker delivers it. The call
site — notifications.send(user, …) — doesn't change at all.
Here's the whole thing, in order.
Mark the notification queueable
Set shouldQueue on the notification. From now on send() routes it through the configured async
dispatcher instead of delivering inline.
export class InvoicePaid implements Notification, MailNotification {
shouldQueue = true;
constructor(private readonly invoiceId: string) {}
via(): string[] {
return ['mail'];
}
toMail(): MailMessage {
return new MailMessage().subject(`Invoice ${this.invoiceId} paid`);
}
}Make the recipient resolvable
A worker can't receive a live entity — only JSON. So a queued notifiable serializes to a small
{ type, id } reference via toNotifiableRef():
export class User implements Notifiable {
constructor(public id: number, public email: string) {}
routeNotificationFor(channel: string) {
return channel === 'mail' ? this.email : undefined;
}
toNotifiableRef(): NotifiableRef {
return { type: 'User', id: this.id };
}
}Register the class and resolver
The worker needs two things to rebuild a job: the notification class (to recreate it from its
serialized name) and a resolveNotifiable function (to reload the recipient from its
reference). Because resolveNotifiable usually needs an injected repository, use forRootAsync.
NotificationsModule.forRootAsync({
inject: [UserRepository],
useFactory: (users: UserRepository) => ({
notifications: [InvoicePaid],
resolveNotifiable: (ref) => users.findOneByOrFail({ id: Number(ref.id) }),
}),
});notifications is the rehydration registry — list every class that may be queued.
resolveNotifiable receives the { type, id } ref and returns the live recipient.
Pick a dispatcher
By default notifications run on the SyncDispatcher — even shouldQueue ones, inline. To
actually offload to a worker, choose a cross-process dispatcher. For production, that's usually
BullMQ:
pnpm add @dudousxd/nestjs-notifications-bullmq @nestjs/bullmq bullmqnpm install @dudousxd/nestjs-notifications-bullmq @nestjs/bullmq bullmqWire it through the module's dispatcher, imports, and providers options — see the
BullMQ dispatcher page for the exact setup.
The rehydration gotcha
The single thing to remember about queued notifications: the constructor does not run again on
the worker. A queued notification serializes to { name, data }, and on the worker the library
rebuilds it by assigning data onto the class prototype. Your via() and to<Channel>() methods
(which live on the prototype) work fine — but anything a constructor computed or fetched is gone.
Keep side effects out of constructors, and don't rely on derived fields surviving the trip. When
you need control over what crosses the wire, override serialize() and add a static
deserialize(data) to the notification — for example, store just an id and refetch on the
worker.
export class InvoicePaid implements Notification {
shouldQueue = true;
constructor(private readonly invoice: Invoice) {}
via() { return ['mail']; }
serialize() {
return { invoiceId: this.invoice.id }; // store only the id
}
static deserialize(data: Record<string, unknown>) {
return new InvoicePaid({ id: data.invoiceId } as Invoice);
}
}Miss toNotifiableRef() on a queued recipient, or configure async without resolveNotifiable, and
the library throws a NotificationSerializationError at dispatch time — telling you exactly what's
missing rather than failing silently on the worker.
See also
- Async dispatch — the full round-trip model and which dispatchers serialize
- Dispatchers — sync, event-emitter, Redis, BullMQ
- Reference: configuration — every
forRoot/forRootAsyncoption
On-demand notifications
Send to a raw address — an email, a Slack webhook, a phone number — without a Notifiable entity. Use notifications.route(channel, value).notify(notification) for one-off and ops alerts.
Real-time & in-app
nestjs-notifications is also your real-time delivery layer. Persist with the database channel, push live with SSE or WebSocket, and consume in React — the whole in-app notification loop, and how to pick SSE vs WebSocket.