NestboltNestbolt

@nestbolt/notifications

Creating Notifications

Learn how to create notification classes by extending the Notification base class, defining delivery channels, and implementing channel-specific methods.

Every notification in @nestbolt/notifications is a plain TypeScript class that extends the Notification base class. Each notification declares which channels it should be delivered through and provides the data for each channel.

The Notification Base Class

The Notification class provides the following methods:

MethodReturn TypeRequiredDescription
via()string[]YesReturns the list of channel names to deliver through.
toDatabase()Record<string, any>NoReturns the data payload for the database channel.
toMail()MailMessageNoReturns a MailMessage for the mail channel.
notificationType()stringNoReturns the notification type identifier. Defaults to the class name.

The only required method is via(). The other methods have default implementations:

  • toDatabase() returns an empty object {}.
  • toMail() throws an error if called without being overridden.
  • notificationType() returns this.constructor.name.

Basic Notification

At minimum, a notification needs to extend Notification and implement via():

import { Notification } from "@nestbolt/notifications";

export class SimpleNotification extends Notification {
  via(): string[] {
    return ["database"];
  }

  toDatabase() {
    return {
      message: "Something happened.",
    };
  }
}

The via() Method

The via() method returns an array of channel name strings. Each string must correspond to a registered channel:

  • "database" -- the built-in database channel
  • "mail" -- the built-in mail channel
  • Any custom channel name you have registered (e.g., "slack", "sms", "webhook")
via(): string[] {
  return ["database", "mail", "slack"];
}

You can use conditional logic to determine which channels to use based on the notification's context:

export class OrderNotification extends Notification {
  constructor(
    private order: Order,
    private urgent: boolean = false,
  ) {
    super();
  }

  via(): string[] {
    const channels = ["database"];

    if (this.order.customer.emailPreferences.orderUpdates) {
      channels.push("mail");
    }

    if (this.urgent) {
      channels.push("sms");
    }

    return channels;
  }
}

The toDatabase() Method

The toDatabase() method returns a plain object that will be serialized as JSON and stored in the data column of the notifications table:

toDatabase() {
  return {
    message: `Your order #${this.order.id} has been shipped.`,
    orderId: this.order.id,
    trackingUrl: this.order.trackingUrl,
    estimatedDelivery: this.order.estimatedDelivery,
  };
}

The return value can be any JSON-serializable object. It is stored using TypeORM's simple-json column type. When you query notifications later, this object is available as notification.data.

The toMail() Method

The toMail() method returns a MailMessage instance built using the fluent builder API:

import { Notification, MailMessage } from "@nestbolt/notifications";

export class InvoiceNotification extends Notification {
  constructor(private invoice: { id: string; amount: number; dueDate: string }) {
    super();
  }

  via(): string[] {
    return ["database", "mail"];
  }

  toDatabase() {
    return {
      message: `Invoice #${this.invoice.id} for $${this.invoice.amount} is due on ${this.invoice.dueDate}.`,
      invoiceId: this.invoice.id,
    };
  }

  toMail() {
    return new MailMessage()
      .subject(`Invoice #${this.invoice.id} - $${this.invoice.amount}`)
      .greeting("Hello")
      .line(`Your invoice #${this.invoice.id} has been generated.`)
      .line(`Amount due: $${this.invoice.amount}`)
      .line(`Due date: ${this.invoice.dueDate}`)
      .action("View Invoice", `https://example.com/invoices/${this.invoice.id}`)
      .salutation("Thank you for your business");
  }
}

If "mail" is in the via() list but toMail() is not overridden, the base class throws an error:

InvoiceNotification must implement toMail() to use the mail channel.

See Mail Channel for the complete MailMessage builder API.

The notificationType() Method

The notificationType() method returns a string identifier stored in the type column of the notifications table. By default, it returns the class name:

class OrderShippedNotification extends Notification {
  // notificationType() returns "OrderShippedNotification" by default
}

Override it to use a custom identifier, which can be useful for decoupling the stored type from your class names or for versioning:

notificationType(): string {
  return "order.shipped";
}

Or use a namespaced format:

notificationType(): string {
  return "App\\Notifications\\OrderShipped";
}

Custom Channel Methods

When you register custom channels, your notification class should implement a corresponding toX() method. While there is no compile-time enforcement for custom methods, the convention is to name them to<ChannelName>():

export class AlertNotification extends Notification {
  constructor(private alert: { message: string; severity: string }) {
    super();
  }

  via(): string[] {
    return ["database", "slack", "sms"];
  }

  toDatabase() {
    return {
      message: this.alert.message,
      severity: this.alert.severity,
    };
  }

  toSlack() {
    return {
      channel: "#alerts",
      text: `[${this.alert.severity.toUpperCase()}] ${this.alert.message}`,
    };
  }

  toSms() {
    return {
      body: `ALERT: ${this.alert.message}`,
    };
  }
}

Your custom channel implementation is responsible for calling the appropriate method on the notification. See Custom Channels for details.

Passing Data to Notifications

Notifications receive their data through the constructor. You can pass any data your notification needs:

export class TeamInviteNotification extends Notification {
  constructor(
    private inviter: { name: string },
    private team: { name: string; id: string },
    private inviteUrl: string,
  ) {
    super();
  }

  via(): string[] {
    return ["database", "mail"];
  }

  toDatabase() {
    return {
      message: `${this.inviter.name} invited you to join ${this.team.name}.`,
      teamId: this.team.id,
      inviterName: this.inviter.name,
    };
  }

  toMail() {
    return new MailMessage()
      .subject(`You have been invited to ${this.team.name}`)
      .greeting("Hello")
      .line(`${this.inviter.name} has invited you to join the team "${this.team.name}".`)
      .action("Accept Invitation", this.inviteUrl)
      .salutation("See you there");
  }
}

Sending Notifications

Once your notification class is defined, send it using NotificationService or the entity mixin:

// Via the service
await notificationService.send(user, new TeamInviteNotification(admin, team, url));

// Via the entity mixin
await user.notify(new TeamInviteNotification(admin, team, url));

// To multiple recipients
await notificationService.sendToMany(
  teamMembers,
  new TeamInviteNotification(admin, team, url),
);

The sendToMany() method iterates over each recipient and calls send() for each one. Each recipient goes through the full channel pipeline independently.

Full Example

Here is a complete notification with multiple channels, conditional routing, and a custom notification type:

import { Notification, MailMessage } from "@nestbolt/notifications";

interface PaymentData {
  id: string;
  amount: number;
  currency: string;
  receiptUrl: string;
  customerName: string;
  failureReason?: string;
}

export class PaymentReceivedNotification extends Notification {
  constructor(private payment: PaymentData) {
    super();
  }

  via(): string[] {
    return ["database", "mail"];
  }

  notificationType(): string {
    return "payment.received";
  }

  toDatabase() {
    return {
      message: `Payment of ${this.payment.currency} ${this.payment.amount} received.`,
      paymentId: this.payment.id,
      amount: this.payment.amount,
      currency: this.payment.currency,
    };
  }

  toMail() {
    return new MailMessage()
      .subject(`Payment received - ${this.payment.currency} ${this.payment.amount}`)
      .greeting(`Hello ${this.payment.customerName}`)
      .line(`We have received your payment of ${this.payment.currency} ${this.payment.amount}.`)
      .line("A receipt has been generated for your records.")
      .action("View Receipt", this.payment.receiptUrl)
      .salutation("Thank you for your payment");
  }
}