Claude
Skills
Sign in
Back

ktgbotapi-patterns

Included with Lifetime
$97 forever

Telegram bot architecture patterns (ktgbotapi 33.x) — project structure, modular handlers, DI via Metro (standalone) or Spring (embedded in backend), callback models, keyboards, utils. Always use the versions listed below; never regress to ktgbotapi 31.x. Koin is no longer recommended — use Metro for compile-time safety.

Backend & APIs

What this skill does


# KTgBotAPI Architecture Patterns

Patterns for organizing Telegram bot projects with ktgbotapi.

## Current Versions (use these — do not downgrade)

| Component | Version | Notes |
|---|---|---|
| ktgbotapi | **33.1.0** | See `ktgbotapi` skill for v32→v33 breaking changes (BotToken value class, Unit return types, Poll API). |
| Metro | **1.0.0** | Default DI for standalone bots — compile-time graph, no runtime crashes. See `metro-di-mobile` skill. |
| Spring Boot | **3.4.x** | Use Spring DI when bot lives inside a full backend (sharing services, DB, observability). See `kotlin-spring-boot` skill. |
| Ktor client | **3.4.3** | See `ktor-client` skill for client patterns. |
| Kotlin | **2.3.21** | |

> **Koin is intentionally not in this skill anymore.** Earlier revisions documented Koin 4.x as the default. We removed it because: (1) compile-time DI catches missing bindings before deploy — Telegram bots can run for weeks without hitting a code path, so a runtime DI miss is found by users, not CI; (2) for monostack setups (mobile + bot + backend on Metro), one DI framework everywhere wins over two; (3) when the bot is part of a Spring backend, Spring DI is already there. If a project genuinely needs Koin (KMP bot sharing modules with a Koin-driven mobile app, or hot-reload module swapping), apply Koin manually — but don't take it from this skill.

## Project Structure

```
src/main/kotlin/com/example/bot/
├── Application.kt              # Entry point
├── di/
│   ├── BotGraph.kt             # Metro @DependencyGraph (standalone)
│   └── BotPlatformModule.kt    # @BindingContainer with @Provides for HttpClient, Json, env config
├── config/
│   └── BotConfig.kt            # Bot configuration value class
├── handlers/
│   ├── CommandHandlers.kt      # /start, /help, etc.
│   ├── MessageHandlers.kt      # Text message handlers
│   ├── CallbackHandlers.kt     # Inline button callbacks
│   └── MediaHandlers.kt        # Photo, document, etc.
├── keyboards/
│   ├── InlineKeyboards.kt      # Inline keyboard builders
│   └── ReplyKeyboards.kt       # Reply keyboard builders
├── fsm/
│   ├── States.kt               # FSM state definitions
│   └── StateHandlers.kt        # State transition handlers
├── api/
│   ├── BackendApiService.kt    # HTTP calls to backend (see ktor-client skill)
│   └── ApiModels.kt            # Request/Response DTOs
├── services/
│   ├── UserService.kt          # Business logic
│   └── NotificationService.kt
├── models/
│   ├── User.kt                 # Domain models
│   └── CallbackData.kt         # Callback payload models
└── utils/
    ├── Extensions.kt           # Useful extensions
    └── Formatters.kt           # Text formatting helpers
```

> **Note:** For backend API communication patterns, see the `ktor-client` skill. Embedded-in-Spring deployment uses Spring's project structure (`@Service`, `@Configuration`) instead of `di/` — see "DI: Spring Boot variant" below.

## Modular Handler Pattern

### Application Entry Point

```kotlin
// Application.kt — standalone bot with Metro
suspend fun main() {
    val graph = createGraph<BotGraph>()
    val bot = graph.telegramBot

    bot.buildBehaviourWithLongPolling(
        defaultExceptionsHandler = { logger.error("Bot error", it) }
    ) {
        with(graph.commandHandlers) { register() }
        with(graph.messageHandlers) { register() }
        with(graph.callbackHandlers) { register() }
        with(graph.mediaHandlers) { register() }
    }.join()
}
```

Each handler class exposes `suspend fun BehaviourContext.register()` as an extension on its enclosing class — see "Modular Handler Pattern" below.

### Handler Modules

Handlers are classes with constructor-injected dependencies. They expose `register()` as an extension on `BehaviourContext`, which keeps the DSL receiver while still letting the handler hold injected services.

```kotlin
// handlers/CommandHandlers.kt
@Inject
class CommandHandlers(
    private val userService: UserService,
) {
    suspend fun BehaviourContext.register() {
        onCommand("start") { message ->
            reply(message, "Welcome!", replyMarkup = ReplyKeyboards.main())
        }

        onCommand("help") { message ->
            reply(message, HelpTexts.commands())
        }

        onCommand("profile") { message ->
            val user = userService.findByChatId(message.chat.id.chatId)
            reply(message, user?.let { "Name: ${it.name}" } ?: "Not registered")
        }

        onDeepLink { message, deepLink -> handleDeepLink(message, deepLink) }
    }
}

// handlers/MessageHandlers.kt
@Inject
class MessageHandlers {
    suspend fun BehaviourContext.register() {
        onText(initialFilter = { it.content.text == "📋 Menu" }) { showMenu(it) }
        onText(initialFilter = { it.content.text == "⚙️ Settings" }) { showSettings(it) }
    }
}

// handlers/CallbackHandlers.kt
@Inject
class CallbackHandlers(
    private val api: BackendApiService,
) {
    suspend fun BehaviourContext.register() {
        onDataCallbackQuery(Regex("menu:.*")) { handleMenuCallback(it) }
        onDataCallbackQuery(Regex("item:.*")) { handleItemCallback(it, api) }
        onDataCallbackQuery(Regex("page:.*")) { handlePaginationCallback(it) }
    }
}
```

> **Why extension on `BehaviourContext` inside a class?** ktgbotapi's DSL (`onCommand`, `onText`, ...) requires `BehaviourContext` as receiver. Free-standing extension functions can't carry constructor state, so we put the extension *inside* the class — `register()` is a member extension, the class holds dependencies, and the DSL receiver flows in at the call site (`with(graph.commandHandlers) { register() }`).

## Callback Data Models

Type-safe callback data parsing:

```kotlin
// models/CallbackData.kt
sealed class CallbackData {
    abstract fun encode(): String

    // Menu actions
    data class Menu(val action: String) : CallbackData() {
        override fun encode() = "m:$action"
    }

    // Item operations
    data class Item(val action: String, val id: String) : CallbackData() {
        override fun encode() = "i:$action:$id"
    }

    // Pagination
    data class Page(val list: String, val page: Int) : CallbackData() {
        override fun encode() = "p:$list:$page"
    }

    // Confirmation
    data class Confirm(val action: String, val id: String) : CallbackData() {
        override fun encode() = "c:$action:$id"
    }

    companion object {
        fun parse(data: String): CallbackData? {
            val parts = data.split(":")
            return when (parts.getOrNull(0)) {
                "m" -> Menu(parts[1])
                "i" -> Item(parts[1], parts[2])
                "p" -> Page(parts[1], parts[2].toInt())
                "c" -> Confirm(parts[1], parts[2])
                else -> null
            }
        }
    }
}

// Usage in handlers
suspend fun BehaviourContext.setupCallbackHandlers() {
    onDataCallbackQuery { query ->
        when (val cb = CallbackData.parse(query.data)) {
            is CallbackData.Menu -> handleMenu(query, cb.action)
            is CallbackData.Item -> handleItem(query, cb.action, cb.id)
            is CallbackData.Page -> handlePage(query, cb.list, cb.page)
            is CallbackData.Confirm -> handleConfirm(query, cb.action, cb.id)
            null -> answer(query, "Unknown action")
        }
    }
}

// Usage in keyboard builders
fun itemKeyboard(itemId: String) = inlineKeyboard {
    row {
        dataButton("✏️ Edit", CallbackData.Item("edit", itemId).encode())
        dataButton("🗑 Delete", CallbackData.Item("delete", itemId).encode())
    }
}
```

## Keyboard Builders

### Inline Keyboards Object

```kotlin
// keyboards/InlineKeyboards.kt
object InlineKeyboards {

    fun mainMenu() = inlineKeyboard {
        row { dataButton("📊 Statistics", "m:stats") }
        row {
            dataButton("👤 Profile", "m:profile")
            dataButton("⚙️ Settings", "m:settings")
        }
        row { urlButton("📖 Help", "https://example.com/help") }
    }

    f

Related in Backend & APIs