Telegram bot template built with mtcute, Bun, TypeScript, and Drizzle ORM. Features a NestJS-inspired modular architecture with dependency injection, decorators, and type safety.
π‘ Also check out TelePlate - Our grammY-based bot template!
- ποΈ Modular Architecture - NestJS-style modules, services, and decorators
- π Dependency Injection - Custom DI container with automatic resolution
- π¨ Decorator-Based Handlers - Clean and intuitive update handling
- ποΈ Drizzle ORM - Type-safe database operations with SQLite
- π Path Aliases - Clean imports (
@core,@common,@database,@modules) - β‘ Bun Runtime - Lightning-fast performance
- π§ Full TypeScript - End-to-end type safety
- π Built-in Logger - Configurable logging system
- βοΈ Environment Validation - Zod-based configuration validation
- π 30+ Event Types - Complete mtcute API coverage
- π Hot Reload - Auto-restart in development mode
Follow these steps to set up and run your bot:
Start by creating a new repository using this template. Click here to create.
Create an environment variables file:
cp .env.example .envEdit .env and set the required variables:
API_ID=12345678
API_HASH=your_api_hash
BOT_TOKEN=your_bot_token
NODE_ENV=development
LOG_LEVEL=debug
SESSION_NAME=bot_session
DATABASE_URL=./bot-data/bot.dbInitialize the database:
bun run db:pushDevelopment Mode:
# Install dependencies
bun install
# Start bot with hot reload
bun run devProduction Mode:
# Install production dependencies only
bun install --production
# Set NODE_ENV to production in .env
# Then start the bot
bun run startbun run devβ Start in development mode with hot reloadbun run startβ Start in production modebun run db:pushβ Push database schemabun run db:studioβ Open Drizzle Studio (database GUI)bun run db:generateβ Generate migrations
project-root/
βββ src/
β βββ core/ # Core framework
β β βββ di/ # Dependency injection
β β β βββ container.ts # DI container
β β β βββ metadata.ts # Metadata keys
β β βββ decorators/ # All decorators
β β β βββ injectable.decorator.ts
β β β βββ inject.decorator.ts
β β β βββ module.decorator.ts
β β β βββ update.decorators.ts # 30+ event decorators
β β β βββ message.decorators.ts # Message filters
β β β βββ callback.decorators.ts # Callback queries
β β β βββ inline.decorators.ts # Inline mode
β β β βββ chat.decorators.ts # Chat events
β β βββ interfaces/
β β β βββ module.interface.ts
β β βββ module-loader.ts # Module loader
β βββ common/ # Common utilities
β β βββ config/ # Configuration
β β β βββ env.schema.ts # Zod validation
β β β βββ env.validator.ts
β β β βββ env.service.ts
β β β βββ config.module.ts
β β βββ logger/ # Logging system
β β βββ logger.service.ts
β β βββ logger.interface.ts
β β βββ logger.module.ts
β βββ database/ # Database layer
β β βββ schema/ # Drizzle schemas
β β β βββ users.schema.ts
β β β βββ chats.schema.ts
β β βββ db.service.ts
β β βββ database.module.ts
β βββ modules/ # Bot modules
β β βββ user/ # User module
β β β βββ user.service.ts # Business logic
β β β βββ user.updates.ts # Update handlers
β β β βββ user.module.ts # Module definition
β β βββ chat/ # Chat module
β β βββ chat.service.ts
β β βββ chat.updates.ts
β β βββ chat.module.ts
β βββ bot.module.ts # Root module
β βββ index.ts # Entry point
βββ drizzle/ # Database migrations
βββ drizzle.config.ts # Drizzle configuration
βββ tsconfig.json # TypeScript config with paths
βββ package.json
βββ .env # Environment variables
// src/modules/hello/hello.service.ts
import { Injectable, Inject } from '@core/decorators';
import { LOGGER } from '@common/logger/constants';
import type { ILogger } from '@common/logger/logger.interface';
@Injectable()
export class HelloService {
constructor(@Inject(LOGGER) private readonly logger: ILogger) {}
getGreeting(name: string): string {
this.logger.log(`Generating greeting for ${name}`);
return `Hello, ${name}! π`;
}
}// src/modules/hello/hello.updates.ts
import { Inject } from '@core/decorators';
import { OnCommand, OnText } from '@core/decorators';
import { TELEGRAM_CLIENT } from '@core/module-loader';
import { HelloService } from './hello.service';
import type { TelegramClient, Message } from '@mtcute/bun';
export class HelloUpdates {
constructor(
@Inject(HelloService) private readonly helloService: HelloService,
@Inject(TELEGRAM_CLIENT) private readonly client: TelegramClient
) {}
@OnCommand('hello')
async handleHello(msg: Message) {
const greeting = this.helloService.getGreeting(msg.sender.firstName);
await this.client.sendText(msg.chat.id, greeting);
}
@OnText(/hi|hey/i)
async handleGreeting(msg: Message) {
await this.client.sendText(msg.chat.id, 'π Hi there!');
}
}// src/modules/hello/hello.module.ts
import { Module } from '@core/decorators';
import { HelloService } from './hello.service';
import { HelloUpdates } from './hello.updates';
import { LoggerModule } from '@common/logger/logger.module';
@Module({
imports: [LoggerModule],
providers: [HelloService],
updates: [HelloUpdates],
exports: [HelloService]
})
export class HelloModule {}Register in bot.module.ts:
import { HelloModule } from '@modules/hello/hello.module';
@Module({
imports: [
ConfigModule,
LoggerModule,
DatabaseModule,
HelloModule, // Add here
]
})
export class BotModule {}@OnCommand('start') // Single command
@OnCommand(['help', 'about']) // Multiple commands
@OnText() // Any text message
@OnText('hello') // Text contains "hello"
@OnText(/pattern/i) // Regex pattern
@OnPhoto() // Photo messages
@OnVideo() // Video messages
@OnAudio() // Audio messages
@OnVoice() // Voice messages
@OnDocument() // Document messages
@OnSticker() // Stickers
@OnAnimation() // GIFs
@OnContact() // Contacts
@OnLocation() // Location
@OnPoll() // Polls
@OnDice() // Dice@OnNewMessage() // New message
@OnEditMessage() // Message edited
@OnDeleteMessage() // Message deleted
@OnMessageGroup() // Album/media group
@OnChatMemberUpdate() // Member status changed
@OnUserStatusUpdate() // User online/offline
@OnUserTyping() // User typing
@OnHistoryRead() // Messages read
@OnBotStopped() // Bot blocked by user
@OnPollUpdate() // Poll updated
@OnPollVote() // Poll vote
@OnStoryUpdate() // Story posted
@OnBotReactionUpdate() // Reaction added@OnCallback() // Any callback query
@OnCallback('button_id') // Specific callback data
@OnCallback(/^action_/) // Regex pattern
@OnInline() // Any inline query
@OnInline('search') // Contains text
@OnChosenInline() // Inline result chosen@OnNewChatMembers() // New members
@OnLeftChatMember() // Member left
@OnPinnedMessage() // Message pinned
@OnNewChatTitle() // Title changed
@OnNewChatPhoto() // Photo changed// src/database/schema/posts.schema.ts
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
userId: integer('user_id').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;import { eq } from 'drizzle-orm';
@Injectable()
export class PostService {
constructor(@Inject(DbService) private readonly db: DbService) {}
async createPost(data: NewPost) {
return await this.db.db.insert(posts).values(data).returning();
}
async getPost(id: number) {
return await this.db.db.query.posts.findFirst({
where: eq(posts.id, id),
});
}
}All configuration is done via environment variables validated with Zod:
// src/common/config/env.schema.ts
export const envSchema = z.object({
API_ID: z.coerce.number().positive(),
API_HASH: z.string().min(1),
BOT_TOKEN: z.string().min(1),
// Add your custom variables
MY_VAR: z.string().default('default_value'),
});Access in services:
constructor(@Inject(ENV_SERVICE) private readonly env: EnvService) {
const apiId = this.env.apiId;
const custom = this.env.get('MY_VAR');
}FROM oven/bun:latest
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
COPY . .
RUN bun run db:push
CMD ["bun", "run", "start"]Build and run:
docker build -t telegram-bot .
docker run -d --env-file .env telegram-bot# Install PM2
bun add -g pm2
# Start
pm2 start bun --name "telegram-bot" -- run start
# Monitor
pm2 logs telegram-bot
pm2 monit- Keep modules focused - One responsibility per module
- Use dependency injection - Better testability and maintainability
- Leverage path aliases - Keep imports clean (
@core,@common, etc.) - Log important events - Use the logger service
- Validate all inputs - Use Zod schemas
- Handle errors gracefully - Wrap handlers in try-catch
- Type everything - Take advantage of TypeScript
- mtcute Documentation - Telegram client library
- Drizzle ORM - TypeScript ORM
- Bun - JavaScript runtime
- TelePlate - grammY-based alternative
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- mtcute - Telegram client library
- NestJS - Architecture inspiration
- Drizzle ORM - Database toolkit
- Bun - Fast all-in-one runtime
If you like this project, please consider giving it a βοΈ on GitHub!
Made with β€οΈ by ByteHolic