A modern, zero-dependency, high-performance, and type-safe Telegram Bot framework for Node.js and TypeScript.
telebot-ts is built from the ground up for modern Node.js environments (v22+). It offers complete coverage of the Telegram Bot API with zero required external runtime dependencies, leveraging native platform features such as globalThis.fetch, node:sqlite, and node:http.
telebot-ts/client, telebot-ts/kernel, telebot-ts/routing, telebot-ts/filters, telebot-ts/storage, telebot-ts/scheduler, telebot-ts/ui).LinearConversation), finite state machines (ConversationHandler), and persistent storage drivers (Memory, JSON file, SQLite).429 Too Many Requests) and server errors (5xx).npm install telebot-ts
import {
ApplicationBuilder,
CommandHandler,
MessageHandler,
filters,
type Update,
type CallbackContext,
} from "telebot-ts";
const app = new ApplicationBuilder().token(process.env.BOT_TOKEN!).build();
// Handle /start command
app.addHandler(
new CommandHandler("start", async (update: Update, context: CallbackContext) => {
const name = update.effective_user?.first_name ?? "friend";
await context.bot.sendMessage({
chat_id: update.effective_chat!.id,
text: `Hello ${name}! Welcome to telebot-ts.`,
});
}),
);
// Echo text messages
app.addHandler(
new MessageHandler(filters.TEXT.and(filters.COMMAND.not()), async (update, context) => {
await context.bot.sendMessage({
chat_id: update.effective_chat!.id,
text: update.effective_message!.text!,
});
}),
);
// Start polling
await app.runPolling({ drop_pending_updates: true });
telebot-ts is structured as modular subpaths, allowing you to import only what you need:
import { Application, ApplicationBuilder } from "telebot-ts"; // Core framework
import { Bot, TelegramApiError } from "telebot-ts/client"; // Low-level Bot API client
import { CommandHandler, MessageHandler } from "telebot-ts/routing"; // Update handlers
import { filters } from "telebot-ts/filters"; // Update and message filters
import { SqlitePersistence, JsonFilePersistence } from "telebot-ts/storage"; // State storage
import { JobQueue } from "telebot-ts/scheduler"; // Background job runner
import { InlineKeyboard, ReplyKeyboard } from "telebot-ts/ui"; // Keyboard layout builders
import { logger } from "telebot-ts/utils"; // Structured logging
Write step-by-step interactive dialogs sequentially in an async/await handler without managing complex state transitions:
import { LinearConversation } from "telebot-ts";
const onboarding = new LinearConversation(async (control) => {
await control.reply("What is your username?");
const usernameUpdate = await control.wait();
const username = usernameUpdate.effective_message?.text ?? "Guest";
await control.reply(`Welcome, ${username}! What is your preferred contact email?`);
const emailUpdate = await control.wait();
const email = emailUpdate.effective_message?.text ?? "Not provided";
await control.reply(`Registration complete for ${username} (${email}).`);
});
app.addHandler(onboarding.createHandler("/register"));
ConversationHandler)For branching workflows with explicit states, fallbacks, and persistence across restarts:
import { ConversationHandler, CommandHandler, MessageHandler, filters } from "telebot-ts";
const STATE_NAME = 1;
const STATE_PHOTO = 2;
const profileHandler = new ConversationHandler({
entry_points: [
new CommandHandler("profile", async (update, context) => {
await context.bot.sendMessage({
chat_id: update.effective_chat!.id,
text: "Please send your profile name:",
});
return STATE_NAME;
}),
],
states: {
[STATE_NAME]: [
new MessageHandler(filters.TEXT, async (update, context) => {
context.user_data.name = update.effective_message!.text;
await context.bot.sendMessage({
chat_id: update.effective_chat!.id,
text: "Now please upload your profile picture (or /skip):",
});
return STATE_PHOTO;
}),
],
[STATE_PHOTO]: [
new MessageHandler(filters.PHOTO, async (update, context) => {
await context.bot.sendMessage({
chat_id: update.effective_chat!.id,
text: `Profile setup complete for ${context.user_data.name}!`,
});
return ConversationHandler.END;
}),
new CommandHandler("skip", async (update, context) => {
await context.bot.sendMessage({
chat_id: update.effective_chat!.id,
text: `Profile saved without photo.`,
});
return ConversationHandler.END;
}),
],
},
fallbacks: [
new CommandHandler("cancel", async (update, context) => {
await context.bot.sendMessage({
chat_id: update.effective_chat!.id,
text: "Profile setup canceled.",
});
return ConversationHandler.END;
}),
],
});
app.addHandler(profileHandler);
Preserve user session data, conversation progress, and bot metadata across application restarts:
import { ApplicationBuilder, SqlitePersistence } from "telebot-ts";
// Native SQLite storage powered by Node.js built-in node:sqlite
const persistence = new SqlitePersistence({
dbPath: "./data/bot_state.sqlite",
});
const app = new ApplicationBuilder().token(process.env.BOT_TOKEN!).persistence(persistence).build();
JobQueue)Schedule delayed timers, recurring interval jobs, or scheduled daily tasks:
app.addHandler(
new CommandHandler("remind", async (update, context) => {
const chatId = update.effective_chat!.id;
// Run a one-shot task in 60 seconds
context.job_queue?.runOnce(async (jobContext) => {
await jobContext.bot.sendMessage({
chat_id: chatId,
text: "Your 60-second reminder is due.",
});
}, 60);
await context.bot.sendMessage({
chat_id: chatId,
text: "Reminder scheduled for 60 seconds from now.",
});
}),
);
Construct clean inline and reply keyboards with a chainable builder API:
import { InlineKeyboard, ReplyKeyboard } from "telebot-ts";
// Inline Keyboard markup
const inlineMenu = new InlineKeyboard()
.text("Confirm", "action_confirm")
.text("Cancel", "action_cancel")
.row()
.url("Documentation", "https://core.telegram.org/bots/api");
// Reply Keyboard markup
const replyMenu = new ReplyKeyboard()
.text("Profile")
.text("Settings")
.row()
.requestContact("Share Contact")
.requestLocation("Share Location")
.resized()
.oneTime();
await bot.sendMessage({
chat_id: 123456,
text: "Choose an option:",
reply_markup: inlineMenu,
});
Deploy on any cloud provider or serverless container behind reverse proxies (Nginx, Cloudflare, AWS ALB) with cryptographic secret token validation:
const app = new ApplicationBuilder().token(process.env.BOT_TOKEN!).build();
// Start native HTTP webhook listener
await app.runWebhook({
listen: "0.0.0.0",
port: 8443,
path: "/telegram-webhook",
secret_token: process.env.WEBHOOK_SECRET,
allowed_updates: ["message", "callback_query", "inline_query"],
});
Send files seamlessly using File IDs, remote URLs, or in-memory binary buffers:
import { InputFile } from "telebot-ts";
import * as fs from "node:fs/promises";
// Upload buffer directly
const buffer = await fs.readFile("./report.pdf");
await bot.sendDocument({
chat_id: 123456,
document: new InputFile(buffer, "Monthly_Report.pdf"),
caption: "Generated report attached.",
});
Integrated zero-dependency structured logger with support for external logging libraries (Pino, Winston):
import { logger } from "telebot-ts";
// Adjust logging verbosity
logger.setLevel("debug");
// Integrate with custom Pino logger
// import pino from "pino";
// const customLogger = pino();
// logger.setLogger(customLogger);
Third-party features ship as self-contained plugins. A plugin is a named object whose synchronous install() receives the Application and registers itself through the extension points below:
| Extension point | API | Use it for |
|---|---|---|
| Plugin registration | app.usePlugin(plugin) |
Entry point; installs once (duplicate names throw), honors dependsOn/priority |
| Middleware | app.use(fn) |
Wrap every update (auth, metrics, attaching sessions) |
| Handlers | app.addHandler() / app.addErrorHandler() |
React to updates or handler failures |
| Lifecycle hooks | app.onInit(hook) / app.onShutdown(hook) |
Async setup/teardown around runPolling / runWebhook / stop |
| Request transforms | bot.transformRequest(fn, tag?) |
Mutate every outgoing API payload (defaults, audit logging) |
| Response/error hooks | bot.transformResponse(fn, tag?) / bot.onApiError(fn, tag?) |
Observe or rewrite results; alert on API failures |
| Namespaced state | app.pluginState(name) |
Private per-plugin state that never collides with bot data |
| Removal | app.removePlugin(name) |
Full deregistration (uninstall hook + everything the plugin registered) |
Installing a plugin:
import { Application, plugins, i18nFor } from "telebot-ts";
const app = new Application(token);
app.usePlugin(
plugins.i18n({
default_locale: "en",
locales: {
en: { hello: "Hello, {name}!" },
vi: { hello: "Xin chào, {name}!" },
},
}),
);
app.on("message", async (update, context) => {
await context.reply(i18nFor(context)!.t("hello", { name: "Nam" }));
});
Writing your own plugin:
import type { Plugin, Application } from "telebot-ts";
export function rateLimitReporter(): Plugin {
return {
name: "my-plugin-rate-limit-reporter",
// Install only after "telebot-plugin-i18n"; lower priority installs first
// among simultaneously ready plugins.
dependsOn: ["telebot-plugin-i18n"],
priority: 0,
install(app: Application) {
// 1. Lifecycle: async setup runs before polling/webhook starts.
app.onInit(async () => {
/* open connections, load state */
});
app.onShutdown(async () => {
/* flush and close */
});
// 2. Middleware: runs around every dispatched update.
app.use(async (context, next) => {
const start = performance.now();
await next();
console.log(`update handled in ${performance.now() - start}ms`);
});
// 3. Request transforms: mutate payloads before they hit the API.
// The tag lets app.removePlugin() detach these hooks cleanly.
app.bot.transformRequest((method, payload) => {
if (method === "sendMessage") payload.protect_content = true;
}, "my-plugin-rate-limit-reporter");
// 4. Namespaced state: private key space, no collisions with bot data.
app.pluginState<{ calls?: number }>("my-plugin-rate-limit-reporter").calls = 0;
},
uninstall(app: Application) {
// Runs before app.removePlugin() deregisters middleware/hooks/handlers.
void app;
},
};
}
app.usePlugin(rateLimitReporter());
// Later, fully detach it:
// app.removePlugin("my-plugin-rate-limit-reporter");
Rules for plugin authors: install() must be synchronous (defer async work to onInit hooks), plugin names must be globally unique, hook failures are isolated — one throwing hook never blocks other plugins or the bot itself — and plugins whose dependsOn dependencies never arrive make runPolling/runWebhook throw at startup.
# Compile TypeScript to ESM and declaration files
npm run build
# Run TypeCheck without emitting files
npm run typecheck
# Run test suite
npm test
# Run tests with code coverage analysis
npm run test:coverage
# Lint source code and tests
npm run lint
# Format codebase with Prettier
npm run format
# Generate TypeDoc API documentation
npm run docs
camelCase (e.g. bot.sendMessage(), app.runPolling(), app.addHandler()).snake_case (e.g. context.user_data, context.chat_data, update.effective_user, chat_id, message_id).MIT License (c) 2026 telebot-ts contributors.