Aviary
Channels

Slack

Post notifications to Slack via an incoming webhook or the Web API. A fluent SlackMessage builder with text, Block Kit blocks, and attachments.

The Slack channel posts a notification to Slack — either through an incoming webhook or the Web API (chat.postMessage) with a bot token. You build the message with a fluent builder that supports plain text, Block Kit blocks, and legacy attachments.

Install

pnpm add @dudousxd/nestjs-notifications-slack
npm install @dudousxd/nestjs-notifications-slack

Register the channel

app.module.ts
import { SlackChannelModule } from '@dudousxd/nestjs-notifications-slack';

@Module({
  imports: [
    // incoming webhook:
    SlackChannelModule.forRoot({ webhookUrl: process.env.SLACK_WEBHOOK_URL }),
    // or, with the Web API:
    // SlackChannelModule.forRoot({ token: process.env.SLACK_BOT_TOKEN, defaultChannel: '#general' }),
  ],
})
export class AppModule {}

SlackChannelModule.forRoot() takes:

OptionTypeDefaultDescription
webhookUrlstringDefault incoming-webhook URL, used when the route doesn't supply one.
tokenstringBot/user token for the Web API. When set, delivery uses chat.postMessage.
defaultChannelstringDefault channel id for Web API delivery when the route isn't a webhook.
resolveOptions(tenant: string) => SlackChannelOptionsPer-tenant options resolver. See Per-tenant config.
globalbooleantrueRegister globally so the channel is discoverable app-wide.

The notification side

Annotate the payload method with the @Slack() handle and return a SlackMessage:

deploy-finished.notification.ts
import { type Notifiable, Notification } from '@dudousxd/nestjs-notifications-core';
import { Slack, SlackMessage } from '@dudousxd/nestjs-notifications-slack';

@Notification()
export class DeployFinished {
  constructor(private service: string) {}

  @Slack()
  toSlack({ notifiable }: ChannelContext): SlackMessage {
    return new SlackMessage()
      .text(`Deploy finished: ${this.service}`)
      .block({ type: 'section', text: { type: 'mrkdwn', text: `*${this.service}* is live :rocket:` } });
  }
}

The Slack handle also works as a via() token (via() { return [Slack]; }) for explicit routing; implement SlackNotification alongside the decorator for compile-time checks on toSlack().

SlackMessage builder

SlackMessage is a fluent builder; each method returns this:

MethodEffect
.text(s)Set the fallback/notification text.
.block(b)Append a Block Kit block. Call repeatedly to add several.
.attachment(a)Append a legacy attachment.
.toPayload()Serialize to the JSON body Slack expects, omitting empty collections.

toPayload() returns a SlackPayload ({ text?, blocks?, attachments? }) — the channel calls it for you, but it's there if you need to inspect the body.

Webhook vs. bot token

The channel picks its delivery mode from the options:

  • Bot token — if token is set, every message goes through the Web API (chat.postMessage) with an Authorization: Bearer header. The channel id comes from the route (a non-URL string) or falls back to defaultChannel.
  • Incoming webhook — without a token, the channel POSTs to a webhook URL. It uses the route if it returned an https:// URL, otherwise the module's webhookUrl.

If neither a webhook URL nor a token is available, the channel throws asking you to provide one.

Routing

routeNotificationFor('slack') controls the target per notifiable. What you return depends on the mode:

team.ts
export class Team implements Notifiable {
  constructor(public slackWebhook: string, public slackChannelId: string) {}

  routeNotificationFor(channel: string) {
    if (channel !== 'slack') return undefined;
    // Webhook mode: return an https:// URL to override the default webhook.
    return this.slackWebhook;
    // Web API mode (token set): return a channel id like '#alerts' or 'C0123456789'.
    // return this.slackChannelId;
  }
}

In webhook mode an https:// route overrides webhookUrl; in Web API mode a non-URL route is used as the channel id, overriding defaultChannel.

Block Kit blocks and attachments take raw Slack JSON, so you have the full Slack message format available — buttons, fields, images, context blocks. Build them in Block Kit Builder and pass each block to .block().

Per-tenant config

In a multi-tenant app each tenant can post to its own workspace. Pass resolveOptions — when a send is scoped to a tenant (via forTenant(id) or a @Tenant() property), the channel uses the options you return (webhook URL, token, default channel) instead of the module defaults:

app.module.ts
SlackChannelModule.forRoot({
  webhookUrl: process.env.SLACK_WEBHOOK_URL,
  resolveOptions: (tenant) => ({ webhookUrl: tenantWebhooks.get(tenant) }),
});

Sends with no tenant scope keep the module defaults. See Multi-tenancy for the full picture.

On this page