Skip to content

Instantly share code, notes, and snippets.

@waptik
Last active October 10, 2025 09:28
Show Gist options
  • Save waptik/2038ad8f167b7af6d25d34ff9b070a2f to your computer and use it in GitHub Desktop.
Save waptik/2038ad8f167b7af6d25d34ff9b070a2f to your computer and use it in GitHub Desktop.
This is a minimal codebase of getting grammY to work with nextjs pages directory. Please use this version of `next-connect`: `"next-connect": "^0.13.0",`
import { Bot, Context } from "grammy";
export const bot = new Bot<Context>(process.env.TELEGRAM_BOT_TOKEN ?? "");
import { bot } from "path/to/bot";
const WEBAPP_URL = "" // URL to your production main site(eg. https://my-secrete-webapp.tld)
const handleGracefulShutdown = async () => {
await bot.stop();
process.exit();
};
if (process.env.NODE_ENV==="development") {
// Graceful shutdown handlers
process.once("SIGTERM", handleGracefulShutdown);
process.once("SIGINT", handleGracefulShutdown);
}
export const startTelegramBotInDev = async () => {
if (!bot.isInited()) {
await bot.start();
}
};
export const startTelegramBotInProduction = async () => {
const webhookUrl = `${WEBAPP_URL}/api/telegram-webhook?token=${env.TELEGRAM_BOT_WEBHOOK_TOKEN}`;
const webhookInfo = await bot.api.getWebhookInfo();
if (webhookInfo.url !== webhookUrl) {
await bot.api.deleteWebhook();
await bot.api.setWebhook(webhookUrl);
}
} catch (_) { }
};
import { NextApiRequest, NextApiResponse } from "next";
import nc from "next-connect";
import { BotError } from "grammy";
import { startTelegramBotInDev } from "path/to/start";
// this is to test the bot locally by visiting http://localhost:3000/api/telegram-dev?action=start
const handler = nc<NextApiRequest, NextApiResponse>({
attachParams: true,
onError: (err, _req, res, next) => {
if (err instanceof BotError) {
res.status(200).send({});
} else {
console.error(err);
res.status(500).end("Something broke!");
}
next();
},
})
.get((req, _res, next) => {
if (process.env.NODE_ENV==="development") {
next();
}
})
.get(async (req: NextApiRequest, res: NextApiResponse) => {
try {
if (req.query && req.query.action !== "start") {
res.status(500).send({ error: { message: "Wrong gateway." } });
return;
}
await startTelegramBotInDev();
res.status(200).send("ok");
} catch (error) {
res.status(500).json({ error });
}
});
export default handler;
// path: /pages/api/telegram-dev.ts
import { NextApiRequest, NextApiResponse } from "next";
import nc from "next-connect";
import { BotError, webhookCallback } from "grammy";
import { bot } from "path/to/bot";
import { startTelegramBotInProduction } from "path/to/start";
const isProd = process.env.NODE_ENV === "production"
const handler = nc<NextApiRequest, NextApiResponse>({
attachParams: true,
onError: (err, _req, res, next) => {
if (err instanceof BotError) {
res.status(200).send({});
} else {
res.status(500).end("Something broke!");
}
next();
},
})
.post((req, _res, next) => {
if (req.query && req.query.token === process.env.TELEGRAM_BOT_WEBHOOK_TOKEN) {
next();
}
})
.post(webhookCallback(bot, "next-js"))
.get(async (req, res) => {
// this is used to automatically setup your webhook by visiting https://my-secrete-webapp.tld/api/telegram-webhook?token=[YOUR-BOT-TOKEN]
// replace [YOUR-BOT-TOKEN] with your telegram bot token
// only do so after you have deployed your bot in production
try {
if (process.env.NODE_ENV !=="production" || (req.query && req.query.token !== process.env.TELEGRAM_BOT_WEBHOOK_TOKEN)) {
return res.status(500).send({ error: { message: "Wrong gateway." } });
}
await startTelegramBotInProduction();
} finally {
return res.status(200).send("ok");
}
});
export default handler;
// path: /pages/api/telegram-webhook.ts
@mortezae
Copy link

mortezae commented Jul 2, 2024

typo in start.ts#L6: console.info

@waptik
Copy link
Author

waptik commented Jul 2, 2024

typo in start.ts#L6: console.info

Thanks! I fixed it and removed non important code from the entire structure so as to minimize unexpected errors

@erfanium
Copy link

did you try this with app directory?

@d0lwl0b
Copy link

d0lwl0b commented Oct 10, 2025

Next.js 15 APP Route webhookCallback use std/htt, no next-js

// src/app/api/bot/telegram/webhook/route.ts
export const dynamic = 'force-dynamic'
export const fetchCache = 'force-no-store'

import { webhookCallback } from "grammy";
import { type NextRequest, NextResponse } from "next/server";
import { appConfig } from "@/lib/config";
import { telegramBot } from "@/services/telegram-bot";

export const ENDPOINT = "/api/bot/telegram/webhook";
const handleUpdate = webhookCallback(telegramBot, "std/http", {
  secretToken: appConfig.bot.telegram.secretToken,
  timeoutMilliseconds: 10000,
  onTimeout: "return",
});

export async function POST(req: NextRequest) {
  try {
    if (!req.body) {
      console.error("Webhook error: Empty request body");
      return NextResponse.json({ error: "Empty request body" }, { status: 400 });
    }

    const contentType = req.headers.get("content-type");
    if (!contentType?.includes("application/json")) {
      console.error(`Webhook error: Invalid Content-Type: ${contentType}`);
      return NextResponse.json({ error: "Invalid Content-Type" }, { status: 400 });
    }

    const response = await handleUpdate(req);
    return response;
  } catch (error) {
    console.error("Webhook error:", error);
    return NextResponse.json({ error: "server error" }, { status: 500 });
  }
}

set bot.api.setWebhook to src/instrumentation.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment