Bot setup in 2 minutes
- Find
@BotFatheron Telegram →/newbot→ set a name and username - Receive a token like
110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw - Open a chat with the bot, send
/start - Call
https://api.telegram.org/botTOKEN/getUpdatesto get yourchat_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
sendOnceto 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