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 CLInpm 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:

  • TelegramBot
  • 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