Если бот живёт дольше 2–3 дней, в нём неизбежно появляется: “быстро поправлю тут”, “временно вот так”, “потом разберусь”. Через месяц это уже не бот, а квест. Ниже - практичный шаблон, который помогает держать код чистым и масштабировать проект без боли.
1️⃣ Главный принцип: разделяй “что бот делает” и “как он общается с Telegram”
❌ Плохой знак: в хендлере 200 строк 0-и там же запросы в БД, логика, тексты, API, платежи. ✅ Хороший знак: хендлер - это тонкий слой: принял апдейт → вызвал сервис → отдал ответ.
Мини‑правило:
- Handlers (Delivery layer) - только роутинг/валидация/ответы
- Services (Use cases) - бизнес‑логика
- Repositories (Data access) - БД/файлы/кэш
- Clients (Integrations) - внешние API
- DTO/Schema - типы входов/выходов между слоями
2️⃣Пример структуры проекта (универсально)
Подойдёт и для aiogram, и для других фреймворков:
bot/
app/ # точка входа, настройка приложения
main.py
config.py
logging.py
handlers/ # обработчики команд/сообщений/кнопок
start.py
orders.py
keyboards/ # кнопки/меню
main.py
texts/ # тексты/шаблоны сообщений
ru.py
services/ # бизнес-логика (use cases)
order_service.py
user_service.py
repositories/ # доступ к данным
user_repo.py
order_repo.py
clients/ # внешние интеграции
payments.py
crm.py
models/ # модели домена / ORM
user.py
order.py
schemas/ # DTO / валидация (pydantic и т.п.)
order.py
utils/ # утилиты
time.py
formatters.py
tests/
test_order_service.py
3️⃣Чистые хендлеры: пример “как должно быть”
Хендлер - короткий, читабельный, без “магии”:
# handlers/orders.py
async def create_order(message, order_service):
user_id = message.from_user.id
text = message.text
order = await order_service.create_order(user_id=user_id, raw_text=text)
await message.answer(f"Заявка #{order.id} принята")
А вся логика - в сервисе:
# services/order_service.py
class OrderService:
def __init__(self, order_repo, anti_spam):
self.order_repo = order_repo
self.anti_spam = anti_spam
async def create_order(self, user_id: int, raw_text: str):
self.anti_spam.check(user_id)
parsed = self._parse(raw_text)
return await self.order_repo.create(user_id=user_id, **parsed)
def _parse(self, raw_text: str) -> dict:
# парсинг/валидация/нормализация
return {"title": raw_text.strip()[:120]}
Почему это важно: сервисы проще тестировать, переиспользовать и менять.
4️⃣ Best practices, которые реально экономят время
Конфиг - только через переменные окружения Токены, ключи, URL - не в коде. Делай config.py, который читает env. Логи вместо print
Логи должны отвечать на 3 вопроса:
- что произошло?
- кого (user_id/чат)?
- чем закончилось (успех/ошибка)?
Тексты отдельно от логики Сегодня “Привет”, завтра - “Здравствуйте”, послезавтра - 2 языка. Если тексты смешаны с кодом - боль гарантирована.
Единый стиль ошибок Определи свои исключения: ValidationError, NotFound, ExternalApiError — и в одном месте решай, как отвечать пользователю.
5️⃣ Тесты: что тестировать в боте в первую очередь
Тестировать Telegram API “в лоб” часто тяжело и не нужно. Тестируй бизнес‑логику (services):
- парсинг и валидация входных данных
- расчёты, условия, ограничения
- работа с репозиториями через моки
- сценарии ошибок (API упало, лимит превышен, пользователь не найден)
6️⃣Чек‑лист “бот готов к росту”
Хендлеры тонкие, логика в сервисах Есть единая структура папок Конфиг через env, секретов в репо нет Тексты вынесены отдельно Логи есть, print нет Есть тесты хотя бы на ключевые сервисы Интеграции изолированы в clients/
Хотите бота без боли - купите бота у меня. Пишите в личку: “Хочу бота” - и коротко опишите задачу (для чего бот и что он должен делать).




Дискуссия