easytg
Build Telegram bot interfaces like pages, not message handlers.
Menus, forms and navigation on top of grammY. Define a screen once and easytg sends, replies or edits it in place.
Getting started · Documentation · Examples · Try it in your browser
Why easytg?
With the plain Bot API, one screen means a command handler that sends it, a callback handler that edits it, answering the callback query, packing state into 64-byte callback_data, and escaping every user-provided string. easytg takes care of that plumbing.
With grammY alone, one screen takes three handlers:
bot.command('start', (ctx) =>
ctx.reply(`Hi <b>${escape(ctx.from.first_name)}</b>`, {
parse_mode: 'HTML',
reply_markup: new InlineKeyboard().text('Order #42', 'order:42'),
}));
bot.callbackQuery(/^order:(.+)$/, async (ctx) => {
const id = ctx.match[1]; // unchecked string
await ctx.editMessageText(`Order <b>${escape(id)}</b>`, {
parse_mode: 'HTML',
reply_markup: new InlineKeyboard().text('Back', 'home'),
});
await ctx.answerCallbackQuery();
});
bot.callbackQuery('home', async (ctx) => {
// …the start screen again, as an edit this time
});With easytg, you describe the screens and easytg sends, edits and answers:
const home = page('home').render(({ ctx, nav }) => ({
text: md`Hi **${ctx.from?.first_name}**`,
keyboard: [[nav.button('Order #42', order, { id: '42' })]],
}));
const order = page<{ id: string }>('order').render(({ params, nav }) => ({
text: md`Order **${params.id}**`,
keyboard: [[nav.button('Back', home)]],
}));
const app = new EasyTG().register(home, order);
bot.use(app);
bot.command('start', (ctx) => app.open(ctx, home));Features
- 🧭 Pages & navigation. Describe screens, not API calls. Send, reply, edit, media swaps and long-text splitting are handled for you.
nav.back()remembers where the user came from, and{ mode: 'send' }keeps a message (like a video) instead of replacing it. - ⌨️ Both keyboards. Inline buttons on pages, plus a main menu on the reply keyboard (
replyMenu) with its own close button. - 🖼 Media. Photos, videos, GIFs, documents, audio, albums and copies of existing messages, with optional
protectContent. - 🔒 Type-safe links.
nav.button('Open', order, { id })fails to compile if a param is missing or misspelled, even when pages link to each other in cycles. - 📝 Dialogues. Multi-step forms with answers typed from the steps, validation (or zod/valibot schemas), choices, several options at once, number pickers, an inline calendar, file uploads, phone number, location and Mini App input, conditional steps, Back/Cancel and timeouts.
- 📱 Mini Apps. Buttons that open them,
initDatachecks, acting for the user from your server, answering into the chat and sharing. - 🔎 Text input on pages.
page.onTextturns a page into a search box or any other typed input. - 🛡️ Secure by default. Escaping
md/htmltemplates, signed or stored tamper-proof button params, params validation with.params(parse)or any Standard Schema, owner-only group menus (orallowedUsers), admin-only pages, and anti-spam with events. - 🌍 Languages. Your messages with
t('cart.items', { count }): placeholders, plural rules, fallbacks and escaping;setLocalefor language pickers, remembered for notifications. - 💾 Sessions and storage. State per user in a chat, per user across chats, and per chat; saved once per update, typed keys, TTLs and migrations. Built-in
MemoryStorage,SqliteStorageandRedisStorage, or any database behind a 3-method interface. - ⏰ Scheduled tasks and queues. Persistent
task()s with retries and recurring runs,deleteLater/sendLater/broadcastLater, self-refreshing pages, and concurrency-limited queues for slow jobs. - 💳 Payments.
invoicepages for Telegram Stars, checkout checks and a success handler. - 🚀 Production-ready. Per-user ordering of concurrent updates, loading indicators for slow pages, a throttle that keeps sends within Telegram's limits, and
clustermode sharing rate limits, locks, queues and throttling across processes (Redis/SQLite). - 🔗 Deep links, inline mode, notifications, broadcasts.
t.me/bot?start=…links, pages as inline results,sendToandapp.editfrom cron jobs and webhooks, pacedbroadcastto thousands of users,autoRetryfor rate limits. - 📊 Events for analytics and monitoring:
update(with timings),pageView,sent, dialogue events,spam,error,payment,queueWait,taskError,broadcastBatch,webAppData. - 🤖 Multi-bot ready. One app instance can serve several bots sharing one storage; sessions, buttons and rate limits stay per bot.
- 🧪 Testable.
easytg/testingruns your bot against a fake Telegram API, with no token and no network. - 💬 Telegram simulator.
easytg/simulatoris an in-memory Telegram with users, groups, buttons, inline mode and payments, plus a chat window for the browser. The docs site uses it for live playgrounds. - 👀 Preview without a token.
bunx easytg preview bot.tsruns your bot file in a simulated Telegram in the browser, restarts it on save, and turns what you click into abun testfile. - ⌘ Commands and relays.
app.commandbinds/commandsto pages and dialogues and syncs Telegram's command menu;app.relaylets two users talk through the bot, replies and edits included. - 🗺 Flow map.
app.flowchart()draws your bot as a Mermaid diagram of pages, dialogues, commands and the buttons between them. - 👁 Only for you, in groups.
mode: 'ephemeral'opens a page or a dialogue from a shared group menu for the presser only (Bot API 10.2 ephemeral messages); ephemeral commands; disabled buttons withnav.disabled. - ✍️ Streaming.
app.streamshows an AI answer as it is written (Telegram drafts in private chats, with a stop button), then sends it as a formatted message. - 📰 Rich messages.
richinstead oftext: headings, tables, task lists, collapsible details, formulas, pictures and buttons in one message (Bot API 10.3), withmdescaping user input for it. - ☁️ Serverless and edge. Webhooks on Cloudflare Workers, Deno Deploy or Vercel, with Redis over HTTP and cron-driven tasks.
- 🪶 Zero dependencies. grammY is the only peer dependency. Bun first; works on Node ≥ 20, Deno and in browsers.
Install
bun add easytg grammynpm install easytg grammyQuick start
import { Bot } from 'grammy';
import { EasyTG, md, page } from 'easytg';
const home = page('home').render(({ ctx, nav }) => ({
text: md`Hello **${ctx.from?.first_name}**!`,
keyboard: [[nav.button('👋 About', about)], [nav.close()]],
}));
const about = page('about').render(({ nav }) => ({
text: 'Built with **easytg**.',
keyboard: [[nav.button('⬅️ Back', home)]],
}));
const bot = new Bot(process.env.BOT_TOKEN!);
const app = new EasyTG().register(home, about);
bot.use(app); // register before your other handlers
bot.command('start', (ctx) => app.open(ctx, home));
bot.start();Next, Getting started walks through building a small shop bot with pages, a form and sessions in about ten minutes.
Core concepts
page(id).render(fn) | A screen. render returns { text, photo, keyboard }, a redirect, or a dialogue to start. Pages can also validate params (.params), show a loading indicator (.loading) and take typed text (.onText). |
nav | Builds buttons and navigation: nav.button, nav.self, nav.back, nav.home, nav.close, nav.url, nav.webApp, nav.pay, nav.deepLink, and the render results nav.redirect / nav.startDialogue. |
replyMenu([...]) | A main menu on the reply keyboard; its buttons open pages or start dialogues. |
dialogue(id).steps([...]) | A multi-step form. Answers arrive in onFinish, typed from the steps. |
session | State of the user in this chat; app.userSession(ctx) follows the user into every chat, app.chatSession(ctx) is shared by a chat. All saved automatically. |
| Middlewares | Run before every page and dialogue, for login checks, admin-only screens and logging. |
app.open(ctx, page) | Shows a page: edits the message when a button was pressed, replies otherwise. |
app.sendTo / app.edit / app.broadcast | Send or update pages without an incoming update: notifications, status cards, newsletters. |
task(id).run(fn) | A job that runs later (app.schedule), saved in storage so it survives restarts. |
// A dialogue in a nutshell
const signup = dialogue('signup')
.steps([
{
id: 'name', type: 'text', text: 'Your name?',
validate: (v) => v.length >= 2 || 'Too short',
},
{
id: 'plan', type: 'choice', text: 'Plan?',
options: [{ text: 'Free', value: 'free' }, { text: 'Pro', value: 'pro' }],
},
])
// answers: { name: string; plan: 'free' | 'pro' }, read off the steps
.onFinish(({ answers }) => ({
text: md`Welcome, ${answers.name}! Plan: ${answers.plan}`,
}));Examples
| Example | Shows |
|---|---|
getting-started.ts | The shop bot from the getting-started guide |
shop.ts | Reply-keyboard menu, catalog with pagination and Back, album gallery, checkout with size, phone number and location |
media.ts | Photo, video, GIF, document (generated), audio and album pages |
search.ts | A searchable catalog with page.onText, in two languages with t() |
course.ts | Members-only videos copied from a storage channel with protectContent, access requests approved by an admin, app.edit, videos that delete themselves (deleteAfterMs) |
reminders.ts | Reminders that survive restarts: scheduled and recurring tasks in SQLite |
queue.ts | Slow AI-style jobs behind a queue: concurrency, one job per user, place in line |
payments.ts | A Telegram Stars shop: invoices, stock check at checkout, delivery, refunds |
inline.ts | Product pages shared with @yourbot query, params checked by a schema, cached photo file ids |
mini-app.ts | A bot and its Mini App in one file: a shop with a checked server API, a color picker step, feedback, sharing |
sessions.ts | State per chat, per user across chats, and per group, side by side |
loading.ts | Slow pages with placeholders, "typing…" and toasts that only show when needed |
throttle.ts | Keeping sends within Telegram's limits, with rules per chat |
group.ts | A group bot: admin-only settings, a self-refreshing scoreboard, a quiz with a timeout |
production.ts | Redis or SQLite, several processes with cluster, webhooks |
broadcast.ts | Newsletter: subscriptions, admin-only compose dialogue, a background broadcast with live progress, broadcastLater, events |
match.ts | A swipe-style "meet people" bot: profile cards with ❤️ 💌 👎 💤 on the reply keyboard, sign-up dialogue, likes, matches and anonymous chats (relay), app.command |
booking.ts | Table reservations: an inline calendar (date), a guest counter (number) and extras (multiChoice) |
support.ts | A support desk: tickets, an agent and a customer talking through the bot (app.relay), a link filter, ratings |
team.ts | A group board where settings and votes open for the presser only (ephemeral pages, dialogue and commands), nav.disabled |
ai-chat.ts | An AI chat whose answers stream in as a rich draft with ⏹ Stop (app.stream), with a fake model |
rich.ts | A handbook of rich messages: a pricing table, collapsible FAQ with a button inside the text, release notes from blocks |
flowchart.ts | A bot that sends its own map (app.flowchart()) |
cloudflare-worker.ts | A Cloudflare Worker: webhook, Redis over HTTP, a Cron Trigger for scheduled tasks |
captcha.ts | Button and typed captchas, server-side answers, per-key TTL |
notify.ts | SQLite storage, tamper-proof buttons, sendLater notifications that survive restarts |
locales/id.ts | Translating the built-in texts |
testing.test.ts | Testing a bot with easytg/testing: navigation, dialogues, errors, broadcasts |
BOT_TOKEN=123:abc bun run examples/captcha.tsDocumentation
- Docs site: these docs with live playgrounds, and every example running in your browser.
- Getting started: a hands-on tutorial.
- Documentation: one page per feature.
- UI: Pages · Media · Main menu · Commands · Flow map · Dialogues · Text input · Text formatting · Rich messages · Languages · Inline mode · Mini Apps · Groups · Streaming
- Data: Sessions · Storage · Button params · Deep links
- Beyond one update: Notifications & broadcast · Relays · Scheduled tasks · Queues · Payments
- Production: Anti-spam · Scaling · Serverless and edge · Security · Events · Error helpers · All options · Testing · Preview · Simulator
- API reference: every export and
appmethod. - Migrating: breaking changes and how to update.
- Changelog
Requirements
- grammY ≥ 1.30
- Bun, Node.js ≥ 20, Deno, or a browser
- TypeScript is optional but recommended: it type-checks navigation.
Contributing
Issues and pull requests are welcome.
bun install
bun test # unit and integration tests (fake Telegram API)
bun run typecheck
bun run build
bun run docs:dev # the docs site with its playgrounds, at localhost:5173Releases are automated. See RELEASING.md.
License
MIT © viandwi24