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=12345678API_HASH=your_api_hashBOT_TOKEN=your_bot_tokenNODE_ENV=developmentLOG_LEVEL=debugSESSION_NAME=bot_sessionDATABASE_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.tsimport{Injectable,Inject}from'@core/decorators';import{LOGGER}from'@common/logger/constants';importtype{ILogger}from'@common/logger/logger.interface';
@Injectable()exportclassHelloService{constructor(@Inject(LOGGER)privatereadonlylogger: ILogger){}getGreeting(name: string): string{this.logger.log(`Generating greeting for ${name}`);return`Hello, ${name}! 👋`;}}// src/modules/hello/hello.updates.tsimport{Inject}from'@core/decorators';import{OnCommand,OnText}from'@core/decorators';import{TELEGRAM_CLIENT}from'@core/module-loader';import{HelloService}from'./hello.service';importtype{TelegramClient,Message}from'@mtcute/bun';exportclassHelloUpdates{constructor(
@Inject(HelloService)privatereadonlyhelloService: HelloService,
@Inject(TELEGRAM_CLIENT)privatereadonlyclient: TelegramClient){}
@OnCommand('hello')asynchandleHello(msg: Message){constgreeting=this.helloService.getGreeting(msg.sender.firstName);awaitthis.client.sendText(msg.chat.id,greeting);}
@OnText(/hi|hey/i)asynchandleGreeting(msg: Message){awaitthis.client.sendText(msg.chat.id,'👋 Hi there!');}}// src/modules/hello/hello.module.tsimport{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]})exportclassHelloModule{}Register in bot.module.ts:
import{HelloModule}from'@modules/hello/hello.module';
@Module({imports: [ConfigModule,LoggerModule,DatabaseModule,HelloModule,// Add here]})exportclassBotModule{}@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.tsimport{sqliteTable,integer,text}from'drizzle-orm/sqlite-core';exportconstposts=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(),});exporttypePost=typeofposts.$inferSelect;exporttypeNewPost=typeofposts.$inferInsert;import{eq}from'drizzle-orm';
@Injectable()exportclassPostService{constructor(@Inject(DbService)privatereadonlydb: DbService){}asynccreatePost(data: NewPost){returnawaitthis.db.db.insert(posts).values(data).returning();}asyncgetPost(id: number){returnawaitthis.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.tsexportconstenvSchema=z.object({API_ID: z.coerce.number().positive(),API_HASH: z.string().min(1),BOT_TOKEN: z.string().min(1),// Add your custom variablesMY_VAR: z.string().default('default_value'),});Access in services:
constructor(@Inject(ENV_SERVICE)privatereadonlyenv: EnvService){constapiId=this.env.apiId;constcustom=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