@yaebal/router

file-based routing — load command and event handlers from a routes/ directory by convention.

install

terminal
pnpm add @yaebal/router

directory convention

place handler files under two sub-directories inside your routes folder. each file must export default a handler function. the filename determines the trigger.

routes/
routes/
  commands/
    start.ts          # → bot.command("start", handler)
    help.ts           # → bot.command("help", handler)
  on/
    message.text.ts   # → bot.on("message:text", handler)
    callback_query.data.ts  # → bot.on("callback_query:data", handler)

files under commands/ map to bot.command(name). files under on/ map to bot.on(query). because : is not a legal character in filenames on all operating systems, use . as a separator — dots are converted to colons automatically (message.text.ts"message:text").

handler files

routes/commands/start.ts
// routes/commands/start.ts
import type { Context } from "@yaebal/core";

export default async (ctx: Context) => {
  await ctx.reply("welcome!");
};
routes/on/message.text.ts
// routes/on/message.text.ts
import type { Context } from "@yaebal/core";

export default async (ctx: Context) => {
  await ctx.reply(`you said: ${ctx.text}`);
};

usage

call loadRoutes(bot, dir) once after constructing the bot. it scans both sub-directories, dynamically imports each file, and registers the default export on the bot. it returns the list of registered route strings — useful for a startup log.

bot.ts
import { Bot } from "@yaebal/core";
import { loadRoutes } from "@yaebal/router";
import { fileURLToPath } from "node:url";
import { join, dirname } from "node:path";

const bot = new Bot(process.env.BOT_TOKEN!);

const __dirname = dirname(fileURLToPath(import.meta.url));
const registered = await loadRoutes(bot, join(__dirname, "routes"));

console.log("registered routes:", registered);
// ["command:start", "command:help", "on:message:text"]

bot.start();

api

exportsignaturedescription
loadRoutes(bot: RouteTarget, dir: string) => Promise<string[]>scans dir/commands/ and dir/on/, registers handlers, returns registered route names
routeFromFile(kind: "commands" | "on", file: string) => \{ method: "command" | "on"; trigger: string \}pure mapping from filename to registration params — exported for testing
RouteTargetinterfaceminimal interface satisfied by Bot and Composer: command() + on()

RouteTarget

methoddescription
command(name, ...handlers)register a command handler
on(query, ...handlers)register an event handler

routeFromFile

the mapping function is exported so you can test your own filename conventions or preview what trigger a filename will produce without touching the filesystem.

mapping.ts
import { routeFromFile } from "@yaebal/router";

routeFromFile("commands", "start.js");
// => { method: "command", trigger: "start" }

routeFromFile("on", "message.text.js");
// => { method: "on", trigger: "message:text" }

routeFromFile("on", "callback_query.data.ts");
// => { method: "on", trigger: "callback_query:data" }
missing directories are silently skipped. if routes/commands/ or routes/on/ does not exist, loadRoutes returns an empty list rather than throwing — useful during incremental setup.

files without a default export are also skipped. only files that export a function as their default export are registered. .d.ts declaration files are always excluded from scanning.

supported extensions: .ts, .js, .mjs.