introduction
yaebal is a type-safe, extensible Telegram Bot API framework where the context type accumulates through a single chainable middleware engine.
what it is
yaebal — yet another tElegram Bot Api Library — wraps the Telegram Bot API in a
chain you build once. you start from a Bot, hang middleware, plugins, routers and
enrichment off it, and every step refines the type of the context your handlers see. there is no
manual casting and no widening to any in the public surface.
import { Bot, bold, format } from "@yaebal/core";
const bot = new Bot(process.env.BOT_TOKEN!)
.derive((ctx) => ({ user: loadUser(ctx.from!.id) })) // async, per-request
.decorate({ version: "1.0.0" }) // static, zero cost
.command("start", (ctx) => ctx.reply("hello 👋"))
.on("message:text", (ctx) => {
ctx.user; // ✅ added by derive, fully typed
ctx.version; // ✅ added by decorate
ctx.text; // ✅ narrowed to string by the filter query
});
bot.start();the design, borrowed deliberately
yaebal takes one strong idea from each of three existing libraries and keeps them distinct:
| source | idea | in yaebal |
|---|---|---|
| GramIO | chainable composer | the context type accumulates through the chain — derive/decorate/install/extend each return an augmented type |
| grammY | filter queries | on("message:text") narrows the context so the filtered field is present and typed |
| puregram | hooks & media | request before/after/onError hooks on the Api, plus a clean MediaSource abstraction |
the core invariants
these are the rules the framework is built to preserve:
- Bot extends Composer. the bot is the middleware chain — the engine is extended, never forked.
- derive is async + per-request; decorate is static.
deriveruns a function on every update to add computed state;decorateattaches a constant value with no per-request cost. they are kept deliberately separate. - types flow through the chain. any composer method that enriches the context
returns a composer with an augmented context type — never widened to
any. - plugin dependencies are explicit. a
Plugin's required context is expressed in its type, so installing it before its dependency is a compile error, not a runtime surprise.
// Bot extends Composer — there is no separate router.
// derive / decorate / install / extend all return the augmented Bot,
// so the context type accumulates as you chain.
class Bot<C extends Context> extends Composer<C> { /* … */ }one engine. because
Bot subclasses Composer, you can
build feature files as plain standalone Composers with no token, then merge them into
the bot with extend — the merged context type comes along.the package map
core is the only package you need to start. it ships the engine, the context, media, hooks and the webhook handlers:
| export | what |
|---|---|
Bot, Composer | the chainable middleware engine |
Context | the per-update wrapper with send/reply/sendPhoto/… |
media, isMediaSource, MediaSource | the file abstraction |
createApi, Api, TelegramError | the low-level API client and hooks |
webhookCallback, nodeWebhookCallback | fetch- and node-style webhook handlers |
format, bold, italic, … | entity-based message formatting helpers |
next
- getting started — a bot from zero in about a minute
- core concepts — the composer, derive/decorate, filter queries