Packages
ex_gram
0.66.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.1
0.55.0
0.54.0
0.53.0
0.52.2
0.52.1
0.52.0
0.51.1
0.51.0
0.50.2
0.50.1
0.50.0
0.41.0
0.40.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.5.0-rc6
0.5.0-rc5
0.5.0-rc4
0.5.0-rc3
0.5.0-rc2
Telegram Bot API low level and framework
Current section
Files
Jump to
Current section
Files
guides/router.md
# Router
As your bot grows, the `handle/2` function can become a long chain of pattern-match clauses — commands, callback queries, text handlers, inline queries, all interleaved in a single module. When you add conversation flows or role-based access, the clause count explodes and it becomes hard to see the overall structure at a glance.
[ExGram.Router](https://hex.pm/packages/ex_gram_router) replaces those hand-written clauses with a declarative **scope / filter / handle** DSL. You describe *what* each handler should match, and the router takes care of dispatching. Everything compiles down to a standard `handle/2` function, so the rest of ExGram (middlewares, DSL, testing) works exactly the same.
> This guide covers the most common usage. For the full API reference and advanced options, see the [ExGram.Router HexDocs](https://hexdocs.pm/ex_gram_router).
## Installation
Add `ex_gram_router` to your dependencies:
```elixir
# mix.exs
def deps do
[
{:ex_gram, "~> 0.65"},
{:ex_gram_router, "~> 0.1.0"},
{:jason, ">= 1.0.0"},
{:req, "~> 0.5"}
]
end
```
Then add `:ex_gram_router` to the formatter deps alongside `:ex_gram`:
```elixir
# .formatter.exs
[
import_deps: [:ex_gram, :ex_gram_router]
]
```
## From `handle/2` to Scopes
Here's a typical bot using plain `handle/2`:
```elixir
defmodule MyBot do
use ExGram.Bot, name: :my_bot, setup_commands: true
command("start", description: "Start the bot")
command("help", description: "Show help")
def handle({:command, :start, _}, ctx), do: answer(ctx, "Welcome!")
def handle({:command, :help, _}, ctx), do: answer(ctx, "Here is what I can do…")
def handle({:text, _, _}, ctx), do: answer(ctx, "You said something!")
def handle(_, ctx), do: ctx
end
```
The same bot with `ExGram.Router`:
```elixir
defmodule MyBot do
use ExGram.Bot, name: :my_bot, setup_commands: true
use ExGram.Router
command("start", description: "Start the bot")
command("help", description: "Show help")
scope do
filter :command, :start
handle &MyBot.Handlers.start/1
end
scope do
filter :command, :help
handle &MyBot.Handlers.help/1
end
scope do
filter :text
handle &MyBot.Handlers.echo/2
end
# Catch-all fallback
scope do
handle &MyBot.Handlers.fallback/1
end
end
```
```elixir
defmodule MyBot.Handlers do
import ExGram.Dsl
def start(ctx), do: answer(ctx, "Welcome!")
def help(ctx), do: answer(ctx, "Here is what I can do…")
# 2-arity: receives (update_info, context) to extract the message text directly
def echo({:text, text, _msg}, ctx), do: answer(ctx, text)
def fallback(ctx), do: ctx
end
```
Notice that handlers live in their own module. This is optional — you can keep them inline — but separating handlers from routing keeps both sides clean as the bot grows.
## Handler Arities
Handlers can be **1-arity** or **2-arity**:
```elixir
# 1-arity: receives only context
def start(context) do
answer(context, "Welcome!")
end
# 2-arity: receives (update_info, context)
def echo({:text, text, _msg}, context) do
answer(context, text)
end
```
The router detects the arity at compile time. Use 2-arity when you need to extract data from the parsed update tuple directly.
### How dispatch works
1. Scopes are tried **top-to-bottom** in declaration order.
2. All filters in a scope must pass (AND logic).
3. The **first matching leaf** wins — its handler runs and dispatch stops.
4. A scope with **no filters** matches everything, so a filter-less scope at the bottom acts as a fallback.
## Built-in Filters
The router ships with filters for the most common update types. Use them by alias name:
```elixir
# Commands
filter :command # any command
filter :command, :start # specific command
# Text messages
filter :text # any text
filter :text, "hello" # exact match
filter :text, ~r/^\d+$/ # regex match
filter :text, prefix: "!" # starts with
filter :text, contains: "hi" # contains substring
# Callback queries
filter :callback_query # any callback
filter :callback_query, "confirm" # exact data
filter :callback_query, ~r/^page_\d+$/ # regex match
filter :callback_query, prefix: "settings:" # prefix match
# Inline queries
filter :inline_query
filter :inline_query, prefix: "@"
# Media & other types
filter :photo
filter :document
filter :location
filter :sticker
filter :voice
filter :video
filter :animation
filter :audio
filter :contact
filter :poll
filter :video_note
filter :message # any message-type update
filter :regex # any named regex match
```
Each filter alias maps to a module under `ExGram.Router.Filters.*`. You never need to reference them directly, but you can if you prefer.
## Nested Scopes
Scopes can be nested. A child scope only runs if its parent's filters already passed, so parent filters act as guards for all children:
```elixir
scope do
filter :callback_query, prefix: "settings:"
scope do
filter :callback_query, "settings:language"
handle &MyBot.Handlers.settings_language/1
end
scope do
filter :callback_query, "settings:timezone"
handle &MyBot.Handlers.settings_timezone/1
end
end
```
This is especially useful for callback queries with hierarchical data patterns. Instead of repeating the prefix in every leaf, you filter it once at the parent level.
### Prefix Propagation
For deeply nested callback data, you can use `propagate: true` on a prefix filter. The matched prefix is stripped and child scopes match against the **remainder**:
```elixir
scope do
filter :callback_query, prefix: "proj:", propagate: true
scope do
filter :callback_query, "change" # matches "proj:change"
handle &MyBot.Handlers.change_project/1
end
scope do
filter :callback_query, prefix: "settings:", propagate: true
scope do
filter :callback_query, "volume" # matches "proj:settings:volume"
handle &MyBot.Handlers.volume/1
end
end
end
```
Propagation stacks across nesting levels, so you can model arbitrary callback data hierarchies cleanly.
## Custom Filters
When the built-in filters aren't enough, you can implement the `ExGram.Router.Filter` behaviour to encode any runtime predicate - user roles, conversation state, feature flags, and more. See the [Custom Filters section in the ExGram.Router documentation](https://github.com/rockneurotiko/ex_gram_router#custom-filters) for the full guide including examples and alias registration.
## Inspecting Routes
The router ships with two mix tasks for visualizing your bot's routing configuration:
### `mix ex_gram.router.tree`
Prints the full scope tree with indentation:
```text
$ mix ex_gram.router.tree MyBot
MyBot routing tree:
├── scope
│ ├── filters: [Command(:start)]
│ └── handle: &MyBot.Handlers.start/1
├── scope
│ ├── filters: [Command(:help)]
│ └── handle: &MyBot.Handlers.help/1
└── scope
├── filters: [CallbackQuery([prefix: "proj:"]) [propagate]]
├── scope
│ ├── filters: [CallbackQuery("change")]
│ └── handle: &MyBot.Handlers.change_project/1
└── scope
├── filters: [CallbackQuery("delete")]
└── handle: &MyBot.Handlers.delete_project/1
```
### `mix ex_gram.router.flat`
Prints one line per handler with the full accumulated filter chain, similar to `mix phx.routes` in Phoenix:
```text
$ mix ex_gram.router.flat MyBot
MyBot handlers:
MyBot.Handlers start/1 filters: [Command(:start)]
MyBot.Handlers help/1 filters: [Command(:help)]
MyBot.Handlers change_project/1 filters: [CallbackQuery([prefix: "proj:"]) [propagate], CallbackQuery("change")]
MyBot.Handlers delete_project/1 filters: [CallbackQuery([prefix: "proj:"]) [propagate], CallbackQuery("delete")]
MyBot.Handlers fallback/1 filters: []
```
These are invaluable for debugging routing issues or onboarding new team members.
## Testing
The router generates a standard `handle/2` function, so testing works exactly like any other ExGram bot. Use `ExGram.Adapter.Test`, push updates, and assert on outgoing API calls. No special setup needed.
See the [Testing](testing.md) guide for details.
## Next Steps
- [FSM](fsm.md) - Add finite state machine conversation flows (works great with the router)
- [Middlewares](middlewares.md) - Enrich context with data your filters need
- [Handling Updates](handling-updates.md) - Understand the update tuples that filters match against
- [Cheatsheet](cheatsheet.md) - Quick reference for common patterns