Skip to content

Migrating from puregram ​

This guide is for developers with a bot written in puregram who want to move to GramIO. Side-by-side comparisons show what changes.


Why migrate? ​

Both are TypeScript-native Telegram bot frameworks. Key differences:

puregramGramIO
Bot initnew Telegram({ token }) / .fromToken()new Bot(token)
Handler registrationbot.updates.on()bot.on()
Starting the botbot.updates.startPolling()bot.start()
Middlewarebot.updates.use()bot.use()
Type-safe context extensionManual augmentation.derive() / .extend() — automatic
FormattingHTML/MarkdownV2 stringsTagged template literals → MessageEntity
hears / text matching@puregram/hear pluginBuilt-in bot.hears()
Scaffolding CLI❌✅ npm create gramio
Built-in test utilities❌✅ @gramio/test
Full Telegram API reference❌✅ /telegram/
Multi-runtime (Node/Bun/Deno)Node.jsNode.js, Bun, Deno

Installation ​

sh
npm install gramio
sh
yarn add gramio
sh
pnpm add gramio
sh
bun add gramio
sh
npm uninstall puregram
sh
yarn remove puregram
sh
pnpm remove puregram
sh
bun remove puregram

Bot initialization ​

ts
import { Telegram } from "puregram";

// Object form
const bot = new Telegram({ token: process.env.BOT_TOKEN! });

// Factory form
const bot = Telegram.fromToken(process.env.BOT_TOKEN!);

bot.updates.startPolling();
ts
import { Bot } from "gramio";

const bot = new Bot(process.env.BOT_TOKEN as string);

bot.start();

Key changes:

  • Telegram → Bot
  • new Telegram({ token }) → new Bot(token) (token is a direct argument)
  • bot.updates.startPolling() → bot.start()

Handlers ​

ts
bot.updates.on("message", (context) => {
    context.reply("Hello!");
});

bot.updates.on("callback_query", (context) => {
    context.answerCallbackQuery("Done!");
});
ts
bot.on("message", (ctx) => {
    ctx.send("Hello!");
});

bot.callbackQuery("my-data", (ctx) => {
    ctx.answer();
});

Key changes:

  • bot.updates.on() → bot.on()
  • context.reply() → ctx.send()
  • context.answerCallbackQuery() → ctx.answer()

Commands ​

ts
bot.updates.on("message", (context) => {
    if (context.text === "/start") {
        context.reply("Welcome!");
    }
});
ts
// Built-in command handler
bot.command("start", (ctx) => ctx.send("Welcome!"));

GramIO has first-class .command() support — no manual text checking needed.


Text matching (hears) ​

ts
import { HearManager } from "@puregram/hear";

const hearManager = new HearManager<MessageContext>();

hearManager.hear(/hello/i, (context) => {
    context.reply("Hey!");
});

bot.updates.on("message", (context, next) =>
    hearManager.middleware(context, next)
);
ts
// No plugin needed
bot.hears(/hello/i, (ctx) => ctx.send("Hey!"));

// String match
bot.hears("hello", (ctx) => ctx.send("Hey!"));

// Function predicate
bot.hears(
    (ctx) => ctx.text?.startsWith("?"),
    (ctx) => ctx.send("A question!"),
);

hears is built into GramIO — the @puregram/hear plugin is not needed.


Context properties ​

ts
context.chat        // TelegramChat
context.from        // TelegramUser
context.senderId    // context.from?.id shorthand
context.message     // TelegramMessage
context.text        // message text
context.callbackQuery  // on callback_query updates
ts
ctx.chat            // TelegramChat
ctx.from            // TelegramUser | undefined
ctx.from?.id        // no special shorthand
ctx.message         // TelegramMessage | undefined
ctx.text            // string | undefined
// callback data is top-level on callbackQuery events

Middleware ​

ts
bot.updates.use(async (context, next) => {
    console.log("Before");
    await next();
    console.log("After");
});
ts
bot.use(async (ctx, next) => {
    console.log("Before");
    await next();
    console.log("After");
});

Key change: bot.updates.use() → bot.use()


Adding data to context ​

ts
// Manual context augmentation via middleware
bot.updates.use(async (context, next) => {
    (context as any).user = await db.getUser(context.from?.id);
    await next();
});

// Must manually type-cast everywhere
const user = (context as any).user;
ts
// Typed automatically — no casts
const bot = new Bot(token)
    .derive(async (ctx) => ({
        user: await db.getUser(ctx.from?.id),
    }));

bot.on("message", (ctx) => {
    ctx.user; // ✅ fully typed, no cast
});
ts
// Static startup-time data (db connection, config)
const bot = new Bot(token)
    .decorate({ db, redis, config });

bot.on("message", (ctx) => {
    ctx.db; // ✅
});

Keyboards ​

Inline keyboard ​

ts
import { Keyboard } from "puregram";

const keyboard = Keyboard.inline([
    [
        Keyboard.textButton({ text: "Yes", payload: "yes" }),
        Keyboard.textButton({ text: "No", payload: "no" }),
    ],
]);

context.reply("Choose:", { reply_markup: keyboard });
ts
import { InlineKeyboard } from "gramio";

const keyboard = new InlineKeyboard()
    .text("Yes", "yes")
    .text("No", "no");

ctx.send("Choose:", { reply_markup: keyboard });

Reply keyboard ​

ts
import { Keyboard } from "puregram";

const keyboard = Keyboard.keyboard([
    [Keyboard.textButton({ text: "Option A" })],
    [Keyboard.textButton({ text: "Option B" })],
]).resize();

context.reply("Choose:", { reply_markup: keyboard });
ts
import { Keyboard } from "gramio";

const keyboard = new Keyboard()
    .text("Option A")
    .row()
    .text("Option B")
    .resized();

ctx.send("Choose:", { reply_markup: keyboard });

Formatting ​

ts
// HTML — manual escaping required
context.reply(
    "<b>Hello</b> <a href='https://gramio.dev'>GramIO</a>",
    { parse_mode: "HTML" }
);

// MarkdownV2 — complex escaping
context.reply("*Bold* _italic_", { parse_mode: "MarkdownV2" });
ts
import { format, bold, italic, link } from "gramio";

// Tagged template literals — no escaping, no parse_mode
ctx.send(
    format`${bold`Hello`} ${link("GramIO", "https://gramio.dev")}`
);

Session ​

ts
import { SessionManager } from "@puregram/session";

const sessionManager = new SessionManager();

bot.updates.use(sessionManager.middleware);

bot.updates.on("message", (context) => {
    context.session.count ??= 0;
    context.session.count++;
    context.reply(`Count: ${context.session.count}`);
});
ts
import { session } from "@gramio/session";

const bot = new Bot(token).extend(
    session({ initial: () => ({ count: 0 }) })
);

bot.on("message", (ctx) => {
    ctx.session.count++;
    //    ^? { count: number } — typed!
    ctx.send(`Count: ${ctx.session.count}`);
});

Scenes ​

ts
import { SceneManager, Scene } from "@puregram/scenes";

const sceneManager = new SceneManager();

const loginScene = new Scene("login");

loginScene.addStep(async (context, next) => {
    await context.reply("Enter your email:");
    await next();
});

loginScene.addStep(async (context) => {
    await context.reply(`Got: ${context.text}`);
});

sceneManager.addScenes([loginScene]);
bot.updates.use(sceneManager.middleware);
ts
import { scenes, Scene } from "@gramio/scenes";
import { session } from "@gramio/session";

const loginScene = new Scene("login")
    .step("message", (ctx) => {
        if (ctx.scene.step.firstTime) return ctx.send("Enter your email:");
        return ctx.scene.update({ email: ctx.text });
    })
    .step("message", (ctx) =>
        ctx.send(`Got: ${ctx.scene.state.email}`)
    );

const bot = new Bot(token)
    .extend(session())
    .extend(scenes([loginScene]));

bot.command("login", (ctx) => ctx.scene.enter(loginScene));

Error handling ​

ts
// Wrap handlers manually
bot.updates.on("message", async (context) => {
    try {
        await riskyOperation();
    } catch (error) {
        context.reply("Something went wrong.");
    }
});
ts
// Global error handler
bot.onError(({ context, error }) => {
    console.error(error);
    if (context.is("message")) context.send("Something went wrong.");
});

// Custom typed errors
class PaymentError extends Error {
    constructor(public reason: string) { super(); }
}

bot
    .error("PAYMENT_FAILED", PaymentError)
    .onError(({ kind, error }) => {
        if (kind === "PAYMENT_FAILED") console.log(error.reason);
    });

Webhook ​

ts
import { Telegram } from "puregram";
import express from "express";

const bot = Telegram.fromToken(token);
const app = express();

app.use(express.json());
app.post("/bot", (req, res) => {
    bot.updates.handleUpdate(req.body);
    res.sendStatus(200);
});

app.listen(3000);
ts
import { Bot, webhookHandler } from "gramio";
import express from "express";

const app = express();
app.use(express.json());
app.post("/webhook", webhookHandler(bot, "express"));
app.listen(3000);

// Tell Telegram where to send updates
bot.start({
    webhook: { url: "https://example.com/webhook" },
});

Sending files ​

ts
import { MediaSource } from "puregram";

context.replyWithPhoto(MediaSource.path("./photo.jpg"));
context.replyWithPhoto(MediaSource.url("https://example.com/photo.jpg"));
context.replyWithPhoto(MediaSource.buffer(buffer, "photo.jpg"));
ts
import { MediaUpload } from "@gramio/files";

ctx.sendPhoto(await MediaUpload.path("./photo.jpg"));
ctx.sendPhoto(await MediaUpload.url("https://example.com/photo.jpg"));
ctx.sendPhoto(await MediaUpload.buffer(buffer, "photo.jpg"));

// Already-uploaded file_id — pass directly
ctx.sendPhoto("AgACAgIAAxk...");

Direct API calls ​

ts
// Via context
await context.telegram.sendMessage({
    chat_id: context.chat.id,
    text: "Hello",
});

// Via bot
await bot.api.sendMessage({
    chat_id: chatId,
    text: "Hello",
});
ts
// Via context
await ctx.api.sendMessage({ chat_id: ctx.chat.id, text: "Hello" });

// Via bot
await bot.api.sendMessage({ chat_id, text: "Hello" });

Both use named params — the API shape is the same.


Lifecycle hooks ​

ts
// Manual setup — no built-in hooks
process.on("SIGINT", () => bot.updates.stopPolling());
ts
bot.onStart(({ info }) => console.log(`@${info.username} started`));
bot.onStop(() => console.log("Shutting down"));

// Graceful shutdown must be set up manually
process.on("SIGINT", () => bot.stop());
process.on("SIGTERM", () => bot.stop());

bot.start();

Quick symbol reference ​

puregramGramIONotes
new Telegram({ token })new Bot(token)Direct token arg
Telegram.fromToken(token)new Bot(token)Same
bot.updates.on()bot.on()Flat, no .updates
bot.updates.use()bot.use()Flat, no .updates
bot.updates.startPolling()bot.start()
context.reply()ctx.send()
context.senderIdctx.from?.idNo shorthand
Keyboard.inline([...])new InlineKeyboard()...Fluent builder
Keyboard.keyboard([...])new Keyboard()...Fluent builder
MediaSource.path()await MediaUpload.path()async
@puregram/hearbot.hears() built-inNo plugin needed
@puregram/session@gramio/session
@puregram/scenes@gramio/scenes
@puregram/prompt@gramio/prompt
parse_mode: "HTML"format\${bold`...`}``No escaping

Next steps ​