Tutorial

Build a Telegram bot in 10 minutes with grammY (Node.js)

You will create a bot, write about 25 lines of JavaScript and talk to it from your phone.

Tutorial2 min readUpdated

Your codenode bot.jsgetUpdates (long polling)sendMessageBot APIapi.telegram.orgTelegramusers1one process only: two pollers cause 409 Conflict
Figure 1. With long polling your process asks Telegram for new updates; no public URL is needed.
  1. Create the bot with @BotFather

    Open a chat with @BotFather (check the blue verification mark), send /newbot, choose a display name and a username that ends in bot. BotFather replies with a token.

    @BotFather/newbotAlright, a new bot. How are we going to call it?Mi Primer BotNow choose a username. It must end in bot.mi_primer_demo_botDone! Use this token: 123456789:AA...1token = password
    Figure 2. Creating the bot with @BotFather (illustration). The last message contains the token: treat it like a password.
  2. Create the project

    mkdir mi-bot && cd mi-bot
    npm init -y
    npm pkg set type=module
    npm install grammy

    type=module lets you use import and top-level await.

  3. Store the token outside the code

    printf 'BOT_TOKEN=123456789:replace-me\n' > .env
    printf '.env\nnode_modules\n' > .gitignore

    Paste your real token in .env. If it ever leaks, use /revoke in BotFather to get a new one.

  4. Write the bot

    bot.js

    import { Bot } from "grammy";
    
    const token = process.env.BOT_TOKEN;
    if (!token) throw new Error("BOT_TOKEN is missing (see .env)");
    const bot = new Bot(token);
    
    bot.command("start", (ctx) => ctx.reply(`Hola, ${ctx.from?.first_name ?? "!"} Send /help.`));
    bot.command("help", (ctx) => ctx.reply("/start - greet\n/help - this list\n/ping - check the bot"));
    bot.command("ping", (ctx) => ctx.reply("pong"));
    
    // Echo any other text message
    bot.on("message:text", (ctx) => ctx.reply(`You said: ${ctx.message.text}`));
    
    // Never crash on a handler error; never log the token
    bot.catch((err) => console.error("handler error:", err.error?.message ?? err.message));
    
    await bot.api.setMyCommands([
      { command: "start", description: "Start the bot" },
      { command: "help", description: "Show commands" },
      { command: "ping", description: "Check the bot is alive" },
    ]);
    bot.start({ onStart: (me) => console.log(`@${me.username} running with long polling`) });
    process.once("SIGINT", () => bot.stop());
    process.once("SIGTERM", () => bot.stop());

    Handlers run in order: commands first, then the generic text handler. bot.catch keeps one bad update from stopping the bot.

  5. Run it and test from your phone

    node --env-file=.env bot.js
    ~/mi-bot$ node --env-file=.env bot.js@mi_primer_demo_bot running with long polling(send /start from your phone)update 1: /start from chat 55512341the bot is online
    Figure 3. Expected output after starting the bot (illustration).

    Open your bot by username, press Start and try /ping. Stop it with Ctrl+C.

  6. Keep it running

    Your laptop will not stay on forever. Deploy the same code to a host that runs 24/7: follow the Railway tutorial (no server to manage) or the Vultr VPS tutorial (full control). Compare options in the hosting comparator.

Troubleshooting

  • 401 Unauthorized: the token is wrong or was revoked. Copy it again from BotFather.
  • 409 Conflict: another process is polling with the same token, or a webhook is set. Stop the other copy; grammY's start() removes a webhook only if you call deleteWebhook first.
  • Bot does not answer in groups: by default privacy mode only delivers commands and replies to the bot. Change it with /setprivacy in BotFather if you really need all messages.

Official docs: grammY getting started, Telegram Bot API.