BOT
MSG
FREE
Telegram Bot
alert • monitor • notify

NestJS Telegram Alerts: Exception Filter, Jobs, and Deploy

Telegram Bot API in NestJS: TelegramModule, exception filter for 5xx alerts, BullMQ job failures, and deploy hooks — no external libraries needed.

11 min read18/06/2026

Bot setup in 2 minutes

  1. Find @BotFather on Telegram → /newbot → set a name and username
  2. Receive a token like 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
  3. Open a chat with the bot, send /start
  4. Call https://api.telegram.org/botTOKEN/getUpdates to get your chat_id
{ "result": [{ "message": { "chat": { "id": 123456789 } } }] }

Add two variables to .env:

TELEGRAM_BOT_TOKEN=110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
TELEGRAM_CHAT_ID=123456789

TelegramModule

Create a dedicated module so TelegramService can be injected anywhere in the app:

// telegram/telegram.service.ts
@Injectable()
export class TelegramService {
  private readonly token: string;
  private readonly chatId: string;
  private readonly cooldowns = new Map<string, number>();

  constructor(private readonly config: ConfigService) {
    this.token = this.config.getOrThrow("TELEGRAM_BOT_TOKEN");
    this.chatId = this.config.getOrThrow("TELEGRAM_CHAT_ID");
  }

  async send(text: string): Promise<void> {
    await fetch(
      `https://api.telegram.org/bot${this.token}/sendMessage`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          chat_id: this.chatId,
          text,
          parse_mode: "HTML",
        }),
      }
    ).catch((err) => {
      // Never let an alert crash the app
      console.error("[Telegram] send failed:", err.message);
    });
  }

  // Send at most once per cooldownMs for a given key
  // Prevents alert storms when errors fire repeatedly
  async sendOnce(key: string, text: string, cooldownMs = 60_000): Promise<void> {
    const now = Date.now();
    if (now - (this.cooldowns.get(key) ?? 0) < cooldownMs) return;
    this.cooldowns.set(key, now);
    await this.send(text);
  }
}
// telegram/telegram.module.ts
@Module({
  imports: [ConfigModule],
  providers: [TelegramService],
  exports: [TelegramService],
})
export class TelegramModule {}

Import into AppModule:

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TelegramModule,
    // ...
  ],
})
export class AppModule {}

Exception filter — production error alerts

Catch all unhandled exceptions and send a Telegram message only for 5xx errors:

// common/filters/all-exceptions.filter.ts
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly telegram: TelegramService) {}

  async catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const req = ctx.getRequest<Request>();
    const res = ctx.getResponse<Response>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof Error ? exception.message : String(exception);

    if (status >= 500) {
      const alert =
        `🔴 <b>Server Error ${status}</b>\n` +
        `<code>${req.method} ${req.url}</code>\n` +
        `<code>${message}</code>`;

      // 1-minute cooldown per endpoint to prevent spam
      await this.telegram.sendOnce(`${req.method}:${req.url}`, alert);
    }

    res.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
    });
  }
}

Register it globally in main.ts:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const telegram = app.get(TelegramService);
  app.useGlobalFilters(new AllExceptionsFilter(telegram));

  await app.listen(3000);
}

Notify on app start and shutdown

Implement OnApplicationBootstrap and OnApplicationShutdown in AppModule:

// app.module.ts
@Module({ ... })
export class AppModule implements OnApplicationBootstrap, OnApplicationShutdown {
  constructor(private readonly telegram: TelegramService) {}

  async onApplicationBootstrap() {
    await this.telegram.send(
      `✅ <b>App started</b>\n` +
      `Env: <code>${process.env.NODE_ENV}</code>\n` +
      `Time: <code>${new Date().toISOString()}</code>`
    );
  }

  async onApplicationShutdown(signal?: string) {
    await this.telegram.send(
      `⚠️ <b>App shutting down</b> (${signal ?? "unknown signal"})`
    );
  }
}

onApplicationShutdown requires shutdown hooks to be enabled:

// main.ts
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // listens for SIGTERM and SIGINT
await app.listen(3000);

Alert on BullMQ job failure

// jobs/email.processor.ts (or any processor)
@Processor("email")
export class EmailProcessor {
  constructor(private readonly telegram: TelegramService) {}

  @Process("send")
  async handleSend(job: Job<{ to: string; subject: string }>) {
    // ... process the job
  }

  @OnQueueFailed()
  async onFailed(job: Job, error: Error) {
    const alert =
      `❌ <b>Job Failed</b>\n` +
      `Queue: <code>${job.queue.name}</code>\n` +
      `Job: <code>${job.name} #${job.id}</code>\n` +
      `Attempts: ${job.attemptsMade}/${job.opts.attempts}\n` +
      `Error: <code>${error.message}</code>`;

    await this.telegram.sendOnce(
      `job-fail:${job.queue.name}:${job.name}`,
      alert,
      5 * 60_000 // 5-minute cooldown per job type
    );
  }
}

Deploy notification from a shell script

When using PM2 with a deploy script:

#!/bin/bash
# deploy.sh

notify() {
  curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
    --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
    --data-urlencode "text=$1" \
    -d "parse_mode=HTML" > /dev/null
}

notify "🚀 Deploying on <b>$(hostname)</b>..."

git pull origin main && \
npm ci --omit=dev && \
npm run build && \
pm2 restart ecosystem.config.js --update-env

if [ $? -eq 0 ]; then
  notify "✅ Deploy succeeded at $(date '+%H:%M:%S')"
else
  notify "❌ Deploy failed on <b>$(hostname)</b>!"
  exit 1
fi

Message formatting helper

parse_mode: "HTML" is easier to control than Markdown — no need to escape special characters in dynamic content:

function formatAlert(opts: {
  emoji: string;
  title: string;
  fields: Record<string, string>;
  code?: string;
}): string {
  const lines = [
    `${opts.emoji} <b>${opts.title}</b>`,
    "",
    ...Object.entries(opts.fields).map(
      ([k, v]) => `<b>${k}:</b> <code>${v}</code>`
    ),
  ];

  if (opts.code) {
    lines.push("", `<pre>${opts.code.slice(0, 800)}</pre>`);
  }

  return lines.join("\n");
}

// Usage in exception filter
const alert = formatAlert({
  emoji: "🔴",
  title: "Server Error 500",
  fields: {
    Method: req.method,
    URL: req.url,
    Error: error.message,
  },
  code: error.stack,
});

Production notes

  • Don't await in the critical path: sending a Telegram message can take 200–500ms. Fire-and-forget in filters and lifecycle hooks
  • Telegram rate limit: 30 messages/second per chat — sufficient for alerting; use sendOnce to prevent storms
  • Token in env only: never commit to git, never log to console
  • Test with curl first: verify token and chat_id work before wiring into code