| name | grammy-bots |
| description | Guides writing grammY Telegram bot handlers, middleware, and plugins. Use when creating or modifying bot commands, inline queries, callback queries, message handlers, or middleware. |
Writing grammY Handlers
Handler File Structure
Each file is a Composer scoped to a business domain (one file per domain):
import { Composer } from "grammy";
import type { Context } from "@/types";
const composer = new Composer<Context>();
const privateChat = composer.chatType("private");
const groupChat = composer.chatType(["group", "supergroup"]);
export default composer;
Register on the error boundary in src/index.ts:
boundary.use(featureHandler);
Filter Chains -- The Core Pattern
NEVER use if + early-return inside handlers. Instead, split every case into separate .filter() chains with mutually exclusive predicates. This applies to .command(), .on(), and all handler methods.
Commands with validation
privateChat.command("find").filter(
(ctx) => ctx.message.reply_to_message?.photo === undefined,
async (ctx) => {
await ctx.reply("Please reply to a photo with /find command.");
},
);
privateChat.command("find").filter(
(ctx) => ctx.message.reply_to_message?.photo !== undefined,
async (ctx) => {
},
);
privateChat.command("find", async (ctx) => {
if (!ctx.message.reply_to_message?.photo) {
await ctx.reply("Please reply to a photo.");
return;
}
});
.on() with .filter() for message routing
Use grammY's filter query language with .on() to narrow update types, then chain .filter() for custom predicates:
feature.on(":text").filter(
(ctx) => ctx.msg.text.startsWith("https://"),
async (ctx) => {
},
);
groupChat.on("message").filter(
(ctx) => shouldReplyToMessage(ctx, ctx.message),
async (ctx) => {
},
);
Filter chain rules
- Each handler processes ONE case atomically
- Predicates must be mutually exclusive and exhaustive
- Validation handlers reply with user-friendly messages
- Processing handlers assume their preconditions are met
grammY Filter Query Language
Examples at https://grammy.dev/guide/filter-queries
Context Type
The project composes context flavors in src/types.ts:
type Context = FileFlavor<HydrateFlavor<BaseContext & ExtendedContext>>;
ExtendedContext adds logger, user, userChat, userChatMember, and currentMessageAttachments. These are attached via middleware. Do not re-declare them.
Separation of Concerns
- Handlers orchestrate: receive update, call services, reply
- Services compute: business logic, no grammY dependency
- Middleware cross-cuts: session, auth, logging
Checklist
- Chat type scoped (
privateChat, groupChat)
- Every handler split into atomic
.filter() chains, no if + early-return
- Business logic in services, not handlers
- Registered on error boundary
Reference
Consult https://grammy.dev for unfamiliar APIs and https://core.telegram.org/bots/api for the Telegram Bot API.