Getting Started
Send your first notification in an existing NestJS app — install the core and a channel, write a notification class, and call send(). Synchronous by default; add a queue when you're ready.
The fastest way to get going is the core plus one channel. Channels register themselves, the
service is injectable everywhere, and delivery is synchronous by default — so you install, write
one class, and call send(). Most apps send their first notification in under five minutes.
Prerequisites
- Node.js 20+
- NestJS 10+ (11 recommended)
@nestjs/event-emitter— the core uses it for lifecycle events
Install
Install the core, the mail channel, and the event emitter:
pnpm add @dudousxd/nestjs-notifications-core @dudousxd/nestjs-notifications-mail @nestjs/event-emitternpm install @dudousxd/nestjs-notifications-core @dudousxd/nestjs-notifications-mail @nestjs/event-emitterImport the modules
Add the modules to your root module. EventEmitterModule.forRoot() powers the lifecycle events;
NotificationsModule.forRoot() provides the service; MailChannelModule.forRoot() registers the
mail channel and is discovered automatically.
import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { NotificationsModule } from '@dudousxd/nestjs-notifications-core';
import { MailChannelModule } from '@dudousxd/nestjs-notifications-mail';
@Module({
imports: [
EventEmitterModule.forRoot(),
NotificationsModule.forRoot(),
MailChannelModule.forRoot({
from: 'billing@example.com',
smtp: { host: 'smtp.example.com', port: 587, auth: { user: '…', pass: '…' } },
}),
],
})
export class AppModule {}Make a recipient notifiable
Mark the per-channel address with @RouteFor — an email here. @Notifiable() + @NotifiableId()
provide the stable reference used if you ever queue (more on that below). Anything can be notifiable:
an entity, a DTO, a plain object.
import { Notifiable, NotifiableId, RouteFor } from '@dudousxd/nestjs-notifications-core';
@Notifiable()
export class User {
@NotifiableId()
id: number;
@RouteFor('mail')
email: string;
constructor(id: number, email: string) {
this.id = id;
this.email = email;
}
}Write a notification
A notification is a plain class. Annotate the payload method with the channel's handle — @Mail() —
and the library infers that this notification goes over the mail channel. No via() array, no
magic strings.
import { Notification } from '@dudousxd/nestjs-notifications-core';
import { Mail, MailMessage } from '@dudousxd/nestjs-notifications-mail';
@Notification()
export class InvoicePaid {
constructor(private invoiceId: string) {}
@Mail()
toMail(): MailMessage {
return new MailMessage()
.subject(`Invoice ${this.invoiceId} paid`)
.greeting('Thanks for your payment!')
.line('We received your payment. No further action is needed.');
}
}Send it
Inject NotificationService and call send. That's the whole flow.
import { Injectable } from '@nestjs/common';
import { NotificationService } from '@dudousxd/nestjs-notifications-core';
import { InvoicePaid } from './invoice-paid.notification';
import { User } from './user';
@Injectable()
export class BillingService {
constructor(private readonly notifications: NotificationService) {}
async paid(user: User, invoiceId: string) {
await this.notifications.send(user, new InvoicePaid(invoiceId));
}
}Add a second channel
Want the same notification to also land in an in-app feed? Add the database channel — no change
to the call site, no via() to edit. Just one more module and one more decorated method.
pnpm add @dudousxd/nestjs-notifications-databasenpm install @dudousxd/nestjs-notifications-databaseimport { DatabaseChannelModule } from '@dudousxd/nestjs-notifications-database';
// inside imports:
DatabaseChannelModule.forRoot(), // bundled in-memory store; swap for a real one laterimport { Database } from '@dudousxd/nestjs-notifications-database';
// add a second decorated method to the class:
@Database()
toDatabase() {
return { invoiceId: this.invoiceId };
}The @Database() decorator adds the channel to the inferred list — there's nothing else to update.
send() now fans out to both. If the mail channel throws, the database row still gets written —
channels are isolated by default. The notifications table is created automatically on first boot
(toggle with autoCreateSchema).
The in-memory store is for development and tests. For production, plug the TypeORM or MikroORM adapter or your own store.
Going async
When you're ready to move delivery off the request, set shouldQueue on the notification and
pick a dispatcher. The call site doesn't change.
@Notification({ name: 'invoice.paid' }) // a stable name keeps async (de)serialization rename-proof
export class InvoicePaid {
shouldQueue = true;
// …
}See Async dispatch for how recipients and notifications are rehydrated inside a worker.
Next steps
- Concepts — notifiables, notifications, channels vs. dispatchers
- Channels — mail, database, broadcast, Slack
- Dispatchers — sync, in-process events, Redis, BullMQ
- Testing — assert sends without delivering
Notifications
Laravel-style notifications for NestJS — define a notification once and deliver it across many channels (mail, database, Slack) and real-time transports (SSE, WebSocket), synchronously or queued.
Notifiables
A notifiable is anything that can receive a notification. Declare per-channel addresses with @RouteFor decorators and mark the id with @NotifiableId — no routeNotificationFor switch, no manual toNotifiableRef.