Aviary
Concepts

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.

A notifiable is anything that can receive a notification — a user entity, a team, a DTO, a plain object. With the decorator API you don't extend a base class or implement an interface: you annotate the properties that hold each channel's address and the recipient stays a normal object you own.

user.ts
import { Notifiable, NotifiableId, RouteFor } from '@dudousxd/nestjs-notifications-core';

@Notifiable()
export class User {
  @NotifiableId()
  id: number;

  @RouteFor('mail')
  email: string;

  @RouteFor('sms')
  phone: string;

  constructor(id: number, email: string, phone: string) {
    this.id = id;
    this.email = email;
    this.phone = phone;
  }
}

That's the whole thing. @RouteFor('mail') says "this property is the mail address"; @RouteFor('sms') says "this one is the SMS address". The recipient owns where a notification goes; the notification owns what it sends.

@RouteFor — per-channel addresses

Put @RouteFor(channel) on the property that holds the address for that channel — an email, a phone number, a Slack webhook, whatever the channel expects. When a notification routes to that channel, the library reads the address straight off the property. No switch, no magic strings sprinkled across a method.

A channel you don't decorate simply has no address; the channel will skip or fall back as documented for that channel.

@Notifiable and @NotifiableId — the async reference

Synchronous delivery passes your live object straight to the channels, so @RouteFor is all you need. The moment a notification is queued, though, it crosses a process boundary: the worker that delivers it has no access to your in-memory User. It only has JSON.

So a queued notifiable must serialize to a stable { type, id } reference the worker can reload. The decorators derive it for you:

  • @Notifiable() marks the class and pins the type. Pass @Notifiable({ type: 'user' }) to set it explicitly; otherwise it defaults to the class name ('User').
  • @NotifiableId() marks the property that holds the id (defaulting to id if omitted).

Together they produce the reference — { type: 'User', id: 7 } — with no toNotifiableRef() to hand-write.

@Notifiable({ type: 'user' }) // type defaults to the class name when omitted
export class User {
  @NotifiableId()
  id: number;
  // …
}

The worker rebuilds the recipient from that reference via the resolveNotifiable function you give NotificationsModule.forRoot(). See Async dispatch for the full round trip.

Reach for @Notifiable() + @NotifiableId() only if the notifiable might be queued. Sync-only apps never need them — and the library throws a clear error if you queue a notifiable that can't build its reference.

Alternative: routeNotificationFor()

The decorator API is the recommended default, but the method-based contract still works and is the right tool for dynamic or computed routing. Implement routeNotificationFor(channel, notification) and it overrides the @RouteFor decorators entirely:

import type { Notification } from '@dudousxd/nestjs-notifications-core';

export class User {
  // …
  routeNotificationFor(channel: string, notification: Notification): unknown {
    if (channel === 'mail' && notification instanceof SecurityAlert) {
      return this.securityEmail ?? this.email; // per-notification choice
    }
    if (channel === 'mail') return this.email;
    if (channel === 'sms') return this.phone;
    return undefined;
  }
}

The second argument is the notification instance, so you can route differently per notification when you need to. You can also mix the styles — keep @RouteFor for the static channels and add routeNotificationFor only when a channel needs computed routing. When present, it wins.

The same applies to the async reference: an explicit toNotifiableRef() overrides @Notifiable() + @NotifiableId(), for when the { type, id } ref must be computed.

On-demand notifiables

Sometimes there's no entity at all — you just want to email an address. You don't need a notifiable for that; use on-demand routing:

alerts.service.ts
@Injectable()
export class AlertsService {
  constructor(private readonly notifications: NotificationService) {}

  async serverDown() {
    await this.notifications.route('mail', 'ops@example.com').notify(new ServerDown());
  }
}

Under the hood that builds an anonymous notifiable that routes the given channel to the given value — the channels can't tell the difference.

On this page