Aiogram 3 вышел из статуса беты в конце 2023 года, и с тех пор я веду на нём все новые Telegram-боты - старые проекты на aiogram 2 переписываю только когда заказчик просит заметно расширить функциональность, потому что менять рабочий код без причины смысла нет. За полтора года практики набралось достаточно кейсов: боты для интернет-магазинов с интеграцией СДЭК и Т‑Банка, бот-помощник с RAG поверх базы знаний, десяток ботов для приёма заявок с сайтов на Tilda и WordPress. Разница с aiogram 2 не косметическая - поменялась архитектура регистрации хендлеров, система фильтров, работа с состояниями и подход к типизации апдейтов через Pydantic.
Роутеры вместо диспетчера - как теперь регистрируются хендлеры
В aiogram 2 все хендлеры вешались напрямую на объект Dispatcher через декораторы вроде @dp.message_handler. Работало нормально, пока бот маленький, но в проекте на 40-50 хендлеров диспетчер превращался в свалку импортов и циклических зависимостей - я сталкивался с этим на боте для магазина, где логика заказов, доставки СДЭК и оплаты через Т‑Банк жила в одном файле handlers.py на полторы тысячи строк.
В aiogram 3 хендлеры собираются в объекты Router, а роутеры потом подключаются к диспетчеру через include_router. Это позволяет резать бота на модули - отдельно роутер для оплаты, отдельно для FSM оформления заказа, отдельно для админки - и подключать их пачкой при старте.
# aiogram 2
from aiogram import Bot, Dispatcher, executor, types
bot = Bot(token="TOKEN")
dp = Dispatcher(bot)
@dp.message_handler(commands=["start"])
async def cmd_start(message: types.Message):
await message.answer("Привет")
executor.start_polling(dp, skip_updates=True)
# aiogram 3
import asyncio
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command
from aiogram.types import Message
router = Router()
@router.message(Command("start"))
async def cmd_start(message: Message):
await message.answer("Привет")
async def main():
bot = Bot(token="TOKEN")
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
asyncio.run(main())
Бот больше не завязан на глобальный объект Bot внутри диспетчера - токен передаётся отдельно при старте polling или webhook. Это удобно, когда нужно поднять несколько ботов на одном коде для разных клиентов, меняя только конфиг.
Магические фильтры вместо lambda-условий
В aiogram 2 для нестандартных условий приходилось писать lambda-фильтры прямо в декораторе или регистрировать отдельные функции-фильтры через register. Читалось это плохо, а дебажить - ещё хуже.
Aiogram 3 принёс magic filter - объект F, который собирает условие в декларативном виде и проверяет поля апдейта без лишнего кода:
from aiogram import F
from aiogram.types import CallbackQuery
@router.callback_query(F.data.startswith("order_"))
async def process_order(callback: CallbackQuery):
order_id = callback.data.split("_")[1]
await callback.answer(f"Заказ {order_id} принят")
F работает не только с data колбэков, но и с текстом сообщений, с полями пользователя, с вложенными объектами апдейта - можно фильтровать по F.text.lower() == "да" или по F.from_user.id.in_(admin_ids). На практике это сократило количество вспомогательных функций-фильтров в проектах примерно вдвое - раньше под каждое нестандартное условие заводился отдельный файл filters.py.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
FSM в aiogram 3: состояния, контекст и хранилища
Машина состояний осталась концептуально похожей на aiogram 2 - те же StatesGroup и State, но интерфейс работы с ними стал асинхронным и явным. Вместо синхронного state.set_state() в aiogram 2 теперь везде await, а данные состояния хранятся через FSMContext, который прокидывается в хендлер как обычный аргумент.
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
class OrderForm(StatesGroup):
name = State()
phone = State()
@router.message(OrderForm.name)
async def process_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(OrderForm.phone)
await message.answer("Укажите телефон")
Хранилища состояний тоже переработаны: из коробки идут MemoryStorage для разработки и RedisStorage для продакшена, а интерфейс хранилища единый, так что переключение между ними - это одна строка в конфиге, а не переписывание логики. На ботах с формой заказа из нескольких шагов (имя, телефон, адрес, способ доставки через СДЭК) это снимает головную боль с потерей состояния при перезапуске процесса - раньше в aiogram 2 память терялась при каждом деплое, если не подключить Redis вручную с самого начала.
Билдеры клавиатур вместо ручной сборки rows
В aiogram 2 инлайн- и обычные клавиатуры собирались через types.InlineKeyboardMarkup(row_width=2) и метод .add(), куда кнопки добавлялись по одной, а логика переноса строк держалась в голове разработчика. В aiogram 3 для этого есть ReplyKeyboardBuilder и InlineKeyboardBuilder - кнопки добавляются списком, а раскладка по строкам задаётся отдельным вызовом adjust:
from aiogram.utils.keyboard import InlineKeyboardBuilder
builder = InlineKeyboardBuilder()
builder.button(text="Оплатить", callback_data="pay")
builder.button(text="Отменить", callback_data="cancel")
builder.adjust(2)
await message.answer("Выберите действие", reply_markup=builder.as_markup())
Удобно, когда количество кнопок формируется динамически - например, список товаров из корзины или доступных пунктов выдачи СДЭК по городу. В aiogram 2 такую динамику приходилось оборачивать в циклы с ручным подсчётом row_width, здесь builder сам раскидывает кнопки по строкам под нужное количество.
Мидлвари и передача данных в хендлеры
Мидлвари в aiogram 2 существовали, но были не особо гибкими - регистрировались через отдельный интерфейс, а данные из мидлвари в хендлер прокидывались через костыльные атрибуты объекта. В aiogram 3 мидлварь - обычный класс с методом __call__, который получает handler, event и словарь data, и может дополнить этот словарь чем угодно перед вызовом хендлера - сессией БД, объектом текущего пользователя, конфигом.
from aiogram import BaseMiddleware
from typing import Callable, Dict, Any, Awaitable
from aiogram.types import TelegramObject
class DbSessionMiddleware(BaseMiddleware):
def __init__(self, session_pool):
self.session_pool = session_pool
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
async with self.session_pool() as session:
data["session"] = session
return await handler(event, data)
Хендлер потом просто принимает session как обычный параметр - aiogram сам подставит его из data по имени. Это ближе к dependency injection, чем к тому, что было в aiogram 2, и заметно сокращает число глобальных переменных и синглтонов в коде бота.
Что переписывать при миграции с aiogram 2 на aiogram 3
Если бот небольшой (до 15-20 хендлеров, без сложного FSM), перевод на aiogram 3 занимает день-два: меняются импорты, регистрация хендлеров переезжает в роутеры, lambda-фильтры превращаются в F. Если бот крупный, с интеграциями оплаты и доставки, миграция растягивается на 3-5 дней - там же обычно всплывают завязки на синхронный API старых версий, которые в aiogram 3 просто убрали в пользу async/await везде.
Отдельно стоит смена типизации: aiogram 3 перешёл на Pydantic 2, и если в проекте были собственные модели данных на Pydantic 1 для сериализации апдейтов, их придётся адаптировать под новые схемы валидации. Плюс поменялись пути импортов у части типов - types.Message в некоторых сборках теперь берётся из aiogram.types, но конкретные классы фильтров и middleware переехали в свои подмодули.
| Параметр | aiogram 2 | aiogram 3 |
|---|---|---|
| Регистрация хендлеров | Через Dispatcher напрямую | Через Router + include_router |
| Фильтры | Lambda и кастомные классы | Magic filter (F) |
| FSM | Синхронные методы | Асинхронный FSMContext |
| Клавиатуры | add() построчно | Builder + adjust() |
| Мидлвари | Ограниченный интерфейс | Полноценный DI через data |
| Типизация | Pydantic 1 | Pydantic 2 |
Если не хочется переписывать хендлеры вручную с нуля, у меня в библиотеке готовых скриптов есть заготовки на aiogram 3 под типовые сценарии - приём заявок, FSM-опросник, интеграция с оплатой. Что касается заказной разработки - веду боты на aiogram 3 под ключ и делаю миграцию существующих проектов с полным тестированием сценариев после переезда.
Автоматизация в мессенджере
Telegram-бот / Mini App
от 30 000 ₽
Подробнее →Частые вопросы
Стоит ли переписывать старого бота на aiogram 3, если он работает на aiogram 2?
Если бот стабилен и не требует новых фич - переписывать ради самой миграции смысла нет, aiogram 2 никуда резко не денется. Смысл появляется, когда нужно добавить сложный FSM-сценарий, подключить Redis-хранилище состояний или упростить структуру кода на 30+ хендлерах - тогда миграция окупается снижением времени на дальнейшую поддержку.
Можно ли использовать aiogram 3 с вебхуками, а не поллингом?
Да, вебхуки поддерживаются нативно через интеграцию с aiohttp - Dispatcher умеет отдавать апдейты через веб-сервер без дополнительных библиотек. Для продакшен-ботов с высокой нагрузкой я обычно ставлю именно вебхуки, поллинг оставляю для разработки и небольших ботов с несколькими сотнями пользователей.
Совместим ли aiogram 3 с FastAPI, если бот встроен в существующий бэкенд?
Совместим, но напрямую не интегрируется - оба фреймворка асинхронные и работают на одном event loop, поэтому вебхук от Telegram можно принимать эндпоинтом FastAPI и передавать апдейт в диспетчер aiogram вручную через feed_update. Такую связку я собирал для проекта, где бот и админ-панель работали в одном процессе.
Сколько стоит разработка бота на aiogram 3 под ключ?
Телеграм-бот у меня начинается от 30 000 ₽ - это простой бот с приёмом заявок и базовым FSM. Интеграции с оплатой, CRM или доставкой (СДЭК, Т‑Банк) считаются отдельно и увеличивают стоимость в зависимости от сложности - точную оценку даю после короткой консультации от 3 000 ₽, где смотрю ТЗ и текущую структуру, если бот уже частично написан.