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.

bot.ts
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:

sourceideain yaebal
GramIOchainable composerthe context type accumulates through the chain — derive/decorate/install/extend each return an augmented type
grammYfilter querieson("message:text") narrows the context so the filtered field is present and typed
puregramhooks & mediarequest 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. derive runs a function on every update to add computed state; decorate attaches 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.ts
// 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:

exportwhat
Bot, Composerthe chainable middleware engine
Contextthe per-update wrapper with send/reply/sendPhoto/…
media, isMediaSource, MediaSourcethe file abstraction
createApi, Api, TelegramErrorthe low-level API client and hooks
webhookCallback, nodeWebhookCallbackfetch- and node-style webhook handlers
format, bold, italic, …entity-based message formatting helpers

next