grammY — SDK Patterns
Idiomatic code patterns for the everyday surface of grammY: creating a Bot, handling updates via the Context object, calling the Bot API, and typing context flavors.
Create a Bot
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN!);
The single positional argument is your BotFather token. A second optional argument lets you pass botInfo, client options (timeouts, base URL), or a custom ContextConstructor.
The Context object (ctx)
Every middleware receives one argument — ctx — which represents the current update being processed. It bundles:
| Property | What it is |
|---|---|
ctx.update |
The full Telegram Update object |
ctx.message, ctx.editedMessage, ctx.channelPost … |
Aliases for the matching update type |
ctx.from |
The sender's User |
ctx.chat |
The current Chat |
ctx.callbackQuery |
Present for inline-button presses |
ctx.api |
Direct Bot API client — ctx.api.sendMessage(chatId, text) |
ctx.me |
Info about the bot itself (filled by bot.init()) |
Reply aliases — always prefer these
Instead of ctx.api.sendMessage(ctx.chat.id, "hi"), write ctx.reply("hi"). grammY provides one alias per Bot API send method, automatically using the current chat id:
await ctx.reply("text");
await ctx.replyWithPhoto("https://example.com/img.png", { caption: "Look" });
await ctx.replyWithDocument(new InputFile("./report.pdf"));
await ctx.replyWithAudio("file_id_from_earlier");
await ctx.replyWithVideo(new InputFile("./demo.mp4"));
await ctx.replyWithSticker("CAACAgIA...");
await ctx.replyWithChatAction("typing");
await ctx.editMessageText("updated text");
await ctx.deleteMessage();
await ctx.answerCallbackQuery({ text: "noted" });
await ctx.answerInlineQuery([...]);
Use ctx.api for other chats
ctx.reply only targets the chat the update came from. To send a message to a different chat (broadcast, admin notification, scheduled post):
await ctx.api.sendMessage(adminChatId, "alert: user reported abuse");
Register handlers
bot.command("start", (ctx) => ctx.reply("Welcome"));
bot.command(["help", "h"], (ctx) => ctx.reply("Help text")); // multiple aliases
bot.hears("ping", (ctx) => ctx.reply("pong")); // plain text match
bot.hears(/^echo (.+)$/i, (ctx) => ctx.reply(ctx.match[1])); // regex with capture
bot.on("message:text", (ctx) => { /* … */ }); // filter query
bot.on("callback_query:data", (ctx) => { /* … */ });
bot.on(":file", async (ctx) => {
const file = await ctx.getFile(); // returns File obj with file_path
console.log(file.file_path);
});
For the full filter-query DSL, open filter-queries.
TypeScript context flavors
The base Context type is augmented by plugins. Whenever you use a plugin that adds methods or properties to ctx, combine its flavor with Context and pass it as a generic to Bot<C>:
import { Bot, Context, SessionFlavor, session } from "grammy";
import { ConversationFlavor, conversations } from "@grammyjs/conversations";
import { HydrateFlavor, hydrate } from "@grammyjs/hydrate";
interface SessionData { counter: number }
type MyContext = HydrateFlavor<
ConversationFlavor<Context & SessionFlavor<SessionData>>
>;
const bot = new Bot<MyContext>(process.env.BOT_TOKEN!);
bot.use(session({ initial: () => ({ counter: 0 }) }));
bot.use(conversations());
bot.use(hydrate());
Order of flavors matches order of bot.use() installation. Forgetting a flavor is the #1 source of Property 'session' does not exist on type 'Context' errors.
API options object
Send methods accept an optional other object with all the Bot API parameters that aren't chat_id / text / file:
await ctx.reply("Bold *italic* `code`", {
parse_mode: "MarkdownV2",
disable_notification: true,
reply_parameters: { message_id: ctx.msg.message_id },
link_preview_options: { is_disabled: true },
});
Initialization (optional, advanced)
By default bot.start() calls bot.init() internally. Call it manually only when:
- You need
ctx.meavailable before the first update (e.g. serverless cold starts — seedeployment-hosting). - You're constructing several
Botinstances and want to skip duplicategetMecalls by passing{ botInfo }from a cache.
await bot.init(); // populates bot.botInfo
console.log(bot.botInfo); // { id, username, ... }
Graceful shutdown
process.once("SIGINT", () => bot.stop());
process.once("SIGTERM", () => bot.stop());
bot.stop() waits for in-flight updates to finish before exiting.
Bot.api — call any Bot API method
bot.api (and ctx.api) is a typed proxy for every Bot API method, plus options:
await bot.api.setMyCommands([
{ command: "start", description: "Begin" },
{ command: "help", description: "Show help" },
]);
await bot.api.setMyDescription("This bot helps you …");
await bot.api.getMe();
await bot.api.deleteWebhook({ drop_pending_updates: true });
The full method list is curated in the api-reference skill (manual-trigger).