Продвинутый курс по Litestar

Курс для разработчиков, которые уже знакомы с основами Litestar и хотят глубже освоить архитектуру, производительность и расширяемость фреймворка. Разберём продвинутую маршрутизацию, DI, работу с БД, безопасность, тестирование и деплой в production.

1. Архитектура Litestar: приложение, роутинг, контроллеры, плагины

Архитектура Litestar: приложение, роутинг, контроллеры, плагины

Litestar — это ASGI-фреймворк для Python, ориентированный на высокую производительность, типизацию и явную архитектуру приложения. В продвинутой разработке главная ценность Litestar — возможность собрать приложение из понятных блоков: объекта приложения, дерева маршрутизации, контроллеров (групп обработчиков) и плагинов (расширений, подключаемых системно).

Эта статья закладывает общий архитектурный каркас курса: дальше мы будем углубляться в DI, работу с БД, OpenAPI, фоновые задачи, тестирование и деплой, опираясь на то, как именно Litestar склеивает обработчики запросов в приложение.

Полезные источники для сверки терминов и деталей:

  • Документация Litestar
  • Репозиторий Litestar на GitHub
  • Спецификация ASGI
  • Starlette (часто используется как ориентир в мире ASGI)
  • !Общая карта компонентов Litestar и поток обработки запроса

    Базовые термины: ASGI и обработчик маршрута

    Чтобы уверенно разбираться в архитектуре Litestar, важно одинаково понимать несколько терминов:

  • ASGI — стандарт взаимодействия асинхронного Python-приложения с ASGI-сервером (например, uvicorn). Сервер принимает сетевые соединения и вызывает приложение как ASGI-callable.
  • Приложение (app) — главный объект Litestar, который хранит конфигурацию, дерево маршрутов, хуки жизненного цикла, плагины и глобальные зависимости.
  • Маршрут (route) — правило сопоставления запроса (путь + HTTP-метод + доп. условия) с обработчиком.
  • Обработчик (handler) — функция или метод (в контроллере), который выполняется при совпадении маршрута.
  • Объект приложения Litestar: что в нём живёт

    В Litestar всё начинается с объекта Litestar. Его удобно воспринимать как композицию нескольких уровней конфигурации.

    Минимальная форма приложения

    Здесь:

  • route_handlers — корневой список обработчиков и/или групп обработчиков (роутеров, контроллеров).
  • get("/health") — декларативное описание маршрута (HTTP GET + путь).
  • Что обычно конфигурируют на уровне приложения

    В продакшн-приложении Litestar(...) часто становится точкой сборки:

  • Глобальные зависимости (Dependency Injection) и их области видимости.
  • Глобальные middleware.
  • Плагины (например, интеграции с ORM или конфигурационные расширения).
  • OpenAPI-конфигурация (описание API, схемы, авторизация).
  • Хуки жизненного цикла (startup/shutdown) и обработчики исключений.
  • Общий state приложения (аккуратно, в основном для ресурсов и конфигов).
  • Жизненный цикл: startup/shutdown и ресурсы

    ASGI-приложение живёт в рамках процесса сервера. Для корректной инициализации соединений (БД, кэш, клиенты внешних API) используют хуки жизненного цикла.

    Типичные задачи на старте:

  • Создать пул соединений к БД.
  • Поднять клиента к Redis.
  • Прогреть конфигурацию.
  • Типичные задачи на остановке:

  • Закрыть пулы и сессии.
  • Дождаться завершения фоновых задач (если применимо).
  • Практическое правило: инициализация ресурса должна быть привязана к жизненному циклу приложения, а не к импорту модулей.

    Роутинг в Litestar: дерево маршрутов и композиция

    Маршрутизация в Litestar — это дерево, которое собирается из обработчиков и группировок. Это удобно для модульности: вы можете включать функциональные блоки (например, users, billing, admin) как независимые поддеревья.

    Маршруты как функции

    Ключевые детали:

  • {user_id:int}типизированный параметр пути. Litestar использует его для валидации и конвертации.
  • Подход “функция как обработчик” хорош для небольших сервисов и для изолированных эндпоинтов.
  • Вложенная маршрутизация через Router

    Когда эндпоинтов становится много, выносите их в Router и монтируйте под префиксом.

    Это даёт:

  • Изоляцию по доменам (каждый модуль возвращает свой Router).
  • Возможность применять настройки на поддерево (guards, dependencies, middleware) точечно.
  • Приоритеты и предсказуемость

    В реальном проекте важно поддерживать предсказуемость сопоставления маршрутов:

  • Избегайте неоднозначных маршрутов на одном уровне (например, /{id:int} и /{slug:str} без явных префиксов).
  • Делайте маршруты семантическими (/users/{id} вместо /{entity}/{id}), если это не API-gateway.
  • Держите версионирование в структуре дерева (/v1, /v2), если требуется параллельная поддержка.
  • Контроллеры: группировка эндпоинтов как класс

    Контроллеры решают проблему “много функций в одном модуле”: они дают единый базовый путь, общие зависимости и удобное группирование методов.

    Контроллер — это класс, методы которого помечены декораторами маршрутов.

    Зачем контроллеры в продвинутой архитектуре

    Контроллеры полезны, когда нужно:

  • Концентрировать общие политики (например, авторизацию) на группу эндпоинтов.
  • Переиспользовать общие зависимости (например, сервисы доменного слоя).
  • Писать более структурированный код (особенно в больших командах).
  • Практическое правило выбора: Router или Controller

    Оба подхода можно комбинировать, но обычно:

  • Controller — когда у вас ресурсная группа эндпоинтов с общими зависимостями и политиками.
  • Router — когда вы собираете модуль из разнородных обработчиков/контроллеров и хотите монтировать его под префиксом.
  • Нормальная структура большого проекта часто выглядит так:

  • Каждый домен (users) экспортирует Router.
  • Внутри домена часть эндпоинтов организована контроллерами.
  • Плагины: расширение Litestar на уровне приложения

    Плагин — это способ подключить расширение на уровне сборки приложения, чтобы оно:

  • модифицировало конфигурацию приложения,
  • добавляло зависимости,
  • подключало обработчики жизненного цикла,
  • интегрировалось со сторонней библиотекой единообразно.
  • В Litestar плагины особенно важны, когда вы хотите избежать копипаста конфигурации между сервисами (например, одинаковая настройка БД, логирования, сериализации, OpenAPI-метаданных).

    Типовой сценарий: подключение готового плагина

    Часто плагины используются для интеграций (например, ORM). Конкретный выбор зависит от стека и версии Litestar, но архитектурно важно понимать следующее:

  • Плагин подключается при создании Litestar(...).
  • Плагин может регистрировать зависимости и хуки.
  • Остальная часть приложения использует уже “готовую” инфраструктуру.
  • Сверяйте актуальный список и синтаксис плагинов в вашей версии:

  • Документация Litestar
  • Архитектурный контракт плагина

    Даже если вы используете готовые плагины, полезно мыслить “как автор плагина”. Хороший плагин:

  • Не протекает деталями реализации в бизнес-код.
  • Минимизирует глобальные сайд-эффекты.
  • Явно описывает, что он добавляет: зависимости, хуки, конфигурацию.
  • Мини-скелет собственного плагина (концептуально)

    Конкретные протоколы и точки расширения могут меняться от версии к версии, поэтому ниже пример демонстрирует идею: централизованно подключить некоторую инфраструктуру и сделать её доступной обработчикам через приложение и/или DI.

    Продвинутая идея, к которой мы вернёмся дальше по курсу: плагин должен создавать ресурсы в жизненном цикле и выдавать их через DI, чтобы обработчики не зависели от глобальных переменных.

    Как эти части складываются в “правильное” приложение

    В продвинутой архитектуре Litestar удобно придерживаться следующей композиции:

  • Входная точка (main.py) содержит только сборку Litestar(...).
  • Каждый домен (users, billing) предоставляет Router.
  • Внутри домена:
  • - Контроллеры группируют эндпоинты по ресурсам. - Бизнес-логика вынесена в сервисы (чистые классы/функции). - Доступ к инфраструктуре (БД/кэш) идёт через DI.
  • Плагины подключают инфраструктуру и общие политики системно, а не “по месту”.
  • Пример “склейки” модулей

    Такой стиль даёт:

  • единый корневой префикс /api,
  • возможность в будущем добавить версионирование (/api/v1),
  • понятные места расширения: middleware, DI, плагины.
  • Частые ошибки в архитектуре Litestar

  • Смешивание сборки приложения и бизнес-логики в одном файле.
  • Инициализация ресурсов на уровне импортов вместо жизненного цикла.
  • Переиспользование глобальных переменных вместо DI.
  • Отсутствие модульного дерева роутинга (когда все обработчики лежат в одном списке).
  • Использование контроллера как “контейнера для всего” без ясных границ домена.
  • Что дальше по курсу

    В следующих материалах мы будем системно наращивать приложение:

  • Dependency Injection и области жизни зависимостей.
  • Middleware и обработка ошибок.
  • Структурирование проекта, конфигурация окружений.
  • Интеграции (БД, миграции, фоновые задачи) через плагины и жизненный цикл.
  • Цель — чтобы к концу курса вы могли поддерживать крупное Litestar-приложение с предсказуемой архитектурой и минимальным “магическим” поведением.

    2. Продвинутая DI: providers, scopes, lifecycle, контекст запроса

    Продвинутая DI: providers, scopes, lifecycle, контекст запроса

    Dependency Injection (DI) в Litestar — это механизм, который склеивает архитектурные блоки из предыдущей темы (приложение, роутеры, контроллеры, плагины) в предсказуемую систему: обработчики остаются тонкими, бизнес-логика живёт в сервисах, инфраструктура создаётся в lifecycle, а “передача” объектов делается через DI вместо глобальных переменных.

    В этой статье мы разберём продвинутые аспекты DI в Litestar:

  • providers (как проектировать и комбинировать поставщиков зависимостей)
  • scopes (где и как живёт объект: на запрос или на приложение)
  • lifecycle (как создавать и закрывать ресурсы правильно)
  • контекст запроса (как безопасно использовать Request/State внутри DI)
  • Рекомендуемые первоисточники:

  • Документация Litestar
  • Раздел usage в документации Litestar
  • !Диаграмма того, как Litestar резолвит зависимости и где они живут

    Что такое DI в Litestar на практике

    DI в Litestar — это соглашение: по имени параметра (и часто по типу) Litestar понимает, что нужно передать в обработчик, и вызывает provider (функцию/вызываемый объект), чтобы получить значение.

    Ключевые элементы:

  • Dependency — “что-то, что нужно обработчику”: настройки, сервис, репозиторий, request-id, транзакция.
  • Provider — фабрика/поставщик зависимости: функция или callable, которую Litestar вызывает, чтобы получить объект.
  • Registration — регистрация зависимости в dependencies=... (на уровне app/router/controller/handler).
  • Caching и scope — правила “как долго живёт объект” и “можно ли переиспользовать результат”.
  • Минимальный пример регистрации зависимости

    Здесь Litestar:

  • видит, что обработчику info() нужен параметр settings
  • находит зарегистрированную зависимость с ключом "settings"
  • вызывает provider provide_settings()
  • передаёт результат в параметр settings
  • Providers: проектирование поставщиков зависимостей

    Provider — это точка, где вы принимаете архитектурные решения. Хороший provider:

  • создаёт объект в правильном месте жизненного цикла
  • не тащит в обработчики детали инфраструктуры
  • прозрачно управляет ресурсами (создание/cleanup)
  • Простые providers: чистые фабрики

    Подход “provider возвращает новый объект” полезен для лёгких и дешёвых зависимостей.

    Плюс: просто.

    Минус: если внутри тяжёлый ресурс (клиент БД, HTTP-клиент), то создавать его на каждый вызов — дорого.

    Providers, которые зависят от других providers

    DI становится особенно полезной, когда зависимости образуют граф: Settings -> Repo -> Service -> Handler.

    Ключевой момент: Litestar может инжектить зависимости внутрь provider, если вы зарегистрировали их в dependencies.

    Практическое правило: provider должен зависеть от интерфейсов и настроек, но не от обработчиков. Тогда граф зависимостей можно переиспользовать между HTTP, задачами и тестами.

    Yield providers: корректный cleanup ресурсов

    Когда зависимость требует освобождения ресурса (закрыть соединение, завершить транзакцию, вернуть объект в пул), используйте provider, который делает yield.

    Общий паттерн:

  • до yield — создать/получить ресурс
  • после yield — гарантированно освободить ресурс
  • Почему это важно: даже если обработчик выбросит исключение, Litestar всё равно выполнит часть после yield и ресурс не утечёт.

    Async providers и sync-to-thread

    Litestar — ASGI-фреймворк, поэтому обработчики часто async. Если provider делает блокирующую работу (например, синхронный драйвер БД), его нельзя бездумно вызывать в event loop.

    Типовые варианты:

  • сделать provider async def и использовать асинхронные библиотеки
  • для синхронного provider использовать настройку sync_to_thread у Provide (если она доступна в вашей версии)
  • Важно: конкретные параметры Provide зависят от версии Litestar, поэтому сверяйте сигнатуру с вашей документацией в разделе usage.

    Scopes: где живут зависимости и почему это важно

    Scope отвечает на вопрос: как долго живёт инстанс зависимости и где можно его безопасно переиспользовать.

    В Litestar на практике чаще всего нужны два scope:

  • request scope — объект создаётся на один запрос и очищается после формирования ответа
  • app scope — объект создаётся один раз и живёт весь срок работы приложения (обычно от startup до shutdown)
  • Что класть в request scope

    Request scope выбирают для объектов, которые:

  • содержат контекст запроса (например, tenant, user, request-id)
  • должны быть изолированы между запросами (например, транзакция или “unit of work”)
  • не должны протекать между конкурентными запросами
  • Примеры:

  • “сессия” работы с БД на запрос
  • объект авторизации, вычисленный из заголовков
  • request-local кэш
  • Что класть в app scope

    App scope выбирают для объектов, которые:

  • дорого создавать
  • потокобезопасны или корректно работают при конкурентном доступе
  • нужны всем запросам
  • Примеры:

  • пул соединений к БД
  • HTTP-клиент, который переиспользует соединения
  • конфигурация (settings), если она неизменяема
  • Scope и кэширование: две стороны одной идеи

    В DI часто есть два механизма, которые работают вместе:

  • scope задаёт “границу жизни”
  • use_cache (или аналог) задаёт “можно ли переиспользовать результат provider внутри этой границы”
  • Типовая логика:

  • request-scoped + cached: “в рамках одного запроса создать один объект и переиспользовать”
  • app-scoped + cached: “создать один объект на приложение”
  • Практический анти-паттерн: случайно сделать request-зависимость app-scoped. Это приводит к утечкам контекста (например, один пользователь “видит” данные другого), потому что объект переживает запрос.

    > Замечание по API: в Litestar 2.x scope обычно задаётся параметром scope у Provide (enum или строка). Точный синтаксис может отличаться, поэтому сверяйте с вашей версией Litestar в документации.

    Lifecycle: как DI связано со startup/shutdown

    DI отвечает за доставку объектов, но не всегда должна отвечать за их создание в правильный момент времени. Для тяжёлых ресурсов (пулы, клиенты, подключение к брокеру) правильная архитектура почти всегда такая:

  • создать ресурс на startup
  • хранить его как app-level ресурс
  • закрыть на shutdown
  • выдавать его в обработчики через DI
  • Паттерн: ресурс в app.state + provider-обёртка

    app.state — место для ресурсов уровня приложения. В обработчики обычно не передают app.state напрямую, а делают provider, чтобы типизировать и стандартизировать доступ.

    Почему это хороший компромисс:

  • lifecycle контролирует создание и закрытие ресурса
  • DI даёт удобное и типизированное “получение” ресурса
  • обработчик не знает, как именно ресурс создавался
  • Паттерн: request-scoped “unit of work” через yield

    Часто нужно, чтобы на каждый запрос:

  • открывалась транзакция/сессия
  • в конце запроса она коммитилась или откатывалась
  • Даже без конкретной ORM можно показать структуру:

    Идея: provider задаёт границу транзакции, а обработчик просто получает DbSession.

    Контекст запроса: Request, state и безопасная передача данных

    “Контекст запроса” — это данные, которые актуальны только внутри одного HTTP-запроса: заголовки, IP клиента, авторизация, request-id, tenant.

    В Litestar контекст обычно доступен через объект запроса, который можно:

  • принять как параметр обработчика
  • принять как параметр provider
  • Инжект Request в обработчик

    Контекст как зависимость: request-id

    Частая задача — корреляция логов. Паттерн: извлечь X-Request-ID или сгенерировать.

    Плюсы подхода:

  • обработчики получают готовое значение
  • при желании можно централизованно добавить логирование/трейсинг
  • request.state становится единым местом для request-local данных
  • Request state против app state

    Правило выбора простое:

  • request.state — данные строго на время запроса
  • app.state — ресурсы и конфигурация на время жизни приложения
  • Нельзя хранить в app.state то, что зависит от конкретного пользователя/запроса.

    Наслоение DI: app, router, controller, handler и переопределение

    DI в Litestar поддерживает композицию так же, как роутинг:

  • зависимости можно задавать на уровне приложения
  • можно задавать на уровне Router
  • можно задавать на уровне Controller
  • можно задавать на уровне конкретного handler
  • Обычно действует принцип: ближе к handler — выше приоритет, то есть локальная зависимость может переопределить глобальную.

    Практические применения:

  • разные политики авторизации для разных поддеревьев роутинга
  • multi-tenant: один и тот же сервис, но с разными настройками в разных Router
  • тестирование: подмена provider на фейк для конкретного набора эндпоинтов
  • Практические рекомендации и частые ошибки

    Рекомендации

  • Делайте providers маленькими: один provider — один смысловой объект.
  • Тяжёлые ресурсы создавайте в lifecycle, а DI используйте для выдачи.
  • Для request-ресурсов с cleanup используйте yield-providers.
  • Для неизменяемых настроек используйте неизменяемые структуры (например, dataclass с frozen=True) и app scope.
  • Следите, чтобы request-зависимости не попадали в app scope.
  • Частые ошибки

  • Создание пула БД при импорте модуля вместо startup.
  • Случайное кэширование зависимости, которая содержит user/request контекст.
  • Передача “сырого” app.state во множество обработчиков вместо одного типизированного provider.
  • Смешивание бизнес-логики и инфраструктуры внутри provider (provider должен собирать объект, а не реализовывать доменные правила).
  • Что дальше

    Теперь, когда DI понятна как “клей” архитектуры Litestar, следующий логический шаг в продвинутом приложении:

  • middleware и централизованная обработка ошибок
  • структурирование проекта и окружений
  • интеграции с БД/ORM и миграциями, где yield-providers и lifecycle становятся критичными
  • 3. Middleware, hooks и guards: расширение поведения и контроль доступа

    Middleware, hooks и guards: расширение поведения и контроль доступа

    Litestar даёт три взаимодополняющих механизма, которые позволяют системно менять поведение приложения без копипаста в каждом обработчике:

  • Middleware — оборачивает обработку запроса целиком и подходит для сквозных задач (логирование, CORS, сжатие, метрики).
  • Hooks — точечные коллбеки, которые исполняются в определённые моменты жизненного цикла запроса/ответа или приложения.
  • Guards — проверка доступа до вызова обработчика (аутентификация/авторизация, tenant-ограничения, feature flags).
  • Связь с предыдущими темами курса:

  • Из статьи про архитектуру важна идея композиции: политики можно применять на уровне Litestar, Router, Controller, конкретного handler.
  • Из статьи про DI важны границы жизни объектов: middleware и guards часто читают контекст запроса (например, request.state), а ресурсы уровня приложения берут из app.state через DI.
  • Рекомендуемые источники:

  • Документация Litestar
  • Репозиторий Litestar на GitHub
  • !Диаграмма порядка выполнения middleware, guards, handler и hooks

    Где выполняются middleware, hooks и guards в потоке запроса

    Типичный порядок (упрощённо):

  • ASGI-сервер вызывает приложение.
  • Выполняются middleware снаружи внутрь.
  • Litestar сопоставляет маршрут.
  • Выполняются guards для найденного обработчика (и унаследованные от router/controller/app).
  • Резолвится DI и вызывается обработчик.
  • Выполняются hooks, привязанные к обработчику/контроллеру/роутеру/приложению (в зависимости от типа hook).
  • Ответ проходит обратно через middleware изнутри наружу.
  • Практический вывод:

  • Middleware хороши для задач, где нужен контроль над всем запросом и ответом.
  • Guards хороши для раннего отказа (дешевле, чем строить DI-граф и вызывать handler).
  • Hooks хороши для локальных политик вокруг handler, когда middleware было бы слишком “глобально”.
  • Middleware: сквозное поведение приложения

    Middleware — это слой, который получает запрос, может изменить его контекст, вызвать следующий слой и затем обработать ответ.

    Когда выбирать middleware

    Используйте middleware, если задача:

  • должна работать для многих (или всех) маршрутов
  • должна видеть и запрос, и ответ
  • должна отрабатывать даже если обработчик не был вызван (например, исключение на ранней стадии)
  • Примеры:

  • request-id и корреляция логов
  • тайминги и метрики
  • трейсинг
  • CORS, GZip, rate limiting
  • Важная модель: порядок и идемпотентность

    Порядок middleware важен:

  • внешний слой “видит” весь запрос целиком, включая работу внутренних слоёв
  • если вы делаете, например, таймер, он должен быть снаружи того, что вы хотите измерить
  • Практическое правило: middleware должны быть идемпотентными и не должны хранить request-specific данные в глобальных переменных. Для request-specific данных используйте request.state.

    Пример: middleware, добавляющее request-id

    Ниже пример концепции: middleware читает заголовок X-Request-ID, а если его нет — генерирует, кладёт в request.state, а затем добавляет в ответ.

    Замечания:

  • Реальный API для middleware в Litestar зависит от версии и может предлагать более удобные базовые классы/типы.
  • Архитектурно важно, что middleware — внешний контур, а значит он подходит для “сквозных” вещей.
  • Типовая ошибка

    Хранить текущий request_id в модульной глобальной переменной. При конкурентных запросах это приведёт к перемешиванию контекста.

    Hooks: точечные расширения вокруг обработчика и ответа

    Hooks — это функции, которые Litestar вызывает в определённые моменты. В отличие от middleware, hooks обычно:

  • привязаны к handler/controller/router/app конфигурации
  • проще по форме
  • удобны для локальных политик (например, “посчитать метрику только для этого контроллера”)
  • Основные группы hooks

    Набор хуков и их сигнатуры могут отличаться по версии, поэтому ориентируйтесь на вашу документацию. На практике чаще всего встречаются такие классы задач:

  • до обработчика (например, подготовка контекста)
  • после обработчика (например, модификация ответа)
  • после отправки ответа (например, логирование результата или фоновые side-effects)
  • хуки жизненного цикла приложения (on_startup, on_shutdown), которые мы уже использовали в теме про DI
  • Пример: after-request hook для добавления заголовка

    Идея: обработчики не должны помнить про технические заголовки. Это можно централизовать хуком.

    Чем hooks отличаются от middleware в этом примере:

  • hook висит на конкретном handler (или его можно повесить на controller/router)
  • он работает после формирования ответа обработчиком
  • он не предназначен для перехвата всего ASGI-конвейера
  • Hooks и DI

    Hooks — хороший потребитель DI, если ваша версия Litestar позволяет инжектить зависимости в hook-функции. Частый паттерн:

  • middleware ставит базовый контекст в request.state (request-id, tenant)
  • hook/handler получает уже готовые значения через DI providers
  • Практическое правило: hooks не должны “создавать инфраструктуру”. Инфраструктура создаётся в lifecycle (on_startup) и выдаётся через DI, как в предыдущей статье.

    Guards: контроль доступа до выполнения обработчика

    Guard — это проверка, которая выполняется до вызова обработчика. Если guard не пропускает запрос, обработчик не выполняется.

    Что удобно решать guards

  • проверку наличия и валидности токена
  • проверку роли/permissions
  • проверку tenant-доступа (например, заголовок X-Tenant)
  • отключение части API через feature flag
  • Плюс guards по сравнению с проверками внутри handler:

  • единообразие и отсутствие копипаста
  • отказ происходит раньше (как правило, быстрее)
  • guard можно повесить на router/controller и применять ко всем эндпоинтам сразу
  • Пример: guard, требующий API key

    Ниже пример охраны роутов по заголовку. Секрет вынесен в app.state, а guard берёт его из request.app.state.

    Почему это продвинутая архитектура:

  • секрет лежит в app.state (уровень приложения)
  • guard читает его из контекста запроса, не используя глобальные переменные
  • обработчик остаётся “чистым”: он предполагает, что доступ уже проверен
  • Наслоение guards на разных уровнях

    Guards можно задавать на:

  • Litestar(...) (глобально)
  • Router (на поддерево)
  • Controller (на группу эндпоинтов)
  • конкретный handler
  • Типовой паттерн для большого проекта:

  • на уровне app: guards, которые применимы почти всегда (например, request-id, базовая защита админки)
  • на уровне router: domain-specific политики (например, billing требует stricter auth)
  • на уровне handler: исключения из правил (например, публичный /health)
  • Как выбрать механизм: краткая сравнительная таблица

    | Задача | Middleware | Hooks | Guards | |---|---|---| | Сквозное логирование, метрики, тайминги | Да | Иногда | Нет | | Добавить заголовок к ответу на группе эндпоинтов | Иногда | Да | Нет | | Проверить право доступа до вызова обработчика | Нет | Иногда | Да | | Работа с ресурсами уровня приложения | Через app.state + DI | Через app.state + DI | Через app.state + DI | | Минимизировать влияние на остальные маршруты | Сложнее | Проще | Проще |

    Практический ориентир:

  • Если вы хотите “обернуть весь запрос” — middleware.
  • Если вы хотите добавить небольшую политику вокруг handler — hooks.
  • Если вы хотите отказать до выполнения handler — guards.
  • Ошибки, которые чаще всего встречаются

  • Использование middleware для авторизации там, где достаточно guard: получается тяжёлый и плохо композиционируемый код.
  • Сохранение user/request контекста в app.state вместо request.state.
  • Смешивание слоёв: например, guard, который создаёт соединение к БД вместо того, чтобы взять готовый ресурс из app lifecycle.
  • Отсутствие тестируемости: если вы “вшили” проверку доступа внутрь handler, её сложнее переиспользовать и проверять изолированно.
  • Что дальше по курсу

    Теперь у вас есть полный набор механизмов расширения поведения:

  • архитектурная композиция (app/router/controller)
  • DI и lifecycle
  • middleware/hooks/guards
  • Следующий логический шаг в продвинутом приложении — централизованная обработка ошибок и наблюдаемость:

  • единый формат ошибок
  • логирование и трассировка
  • интеграция с БД и транзакциями, где yield-providers и хуки/guards помогают стандартизировать границы операций
  • 4. Валидация и сериализация: DTO, OpenAPI, кастомные схемы

    Валидация и сериализация: DTO, OpenAPI, кастомные схемы

    В продвинутом приложении на Litestar важны три вещи:

  • Предсказуемая валидация входных данных на границе системы
  • Стабильная сериализация ответов без утечек внутренних полей
  • Контракт API, который автоматически документируется и проверяется через OpenAPI
  • Связь с предыдущими темами курса:

  • Из статьи про архитектуру берём принцип композиции: настройки валидации и документации должны накладываться на уровне app, router, controller, handler.
  • Из статьи про DI берём принцип “инфраструктура живёт в lifecycle, бизнес-логика не знает про фреймворк”: DTO должны быть границей транспорта, а доменные объекты и сервисы должны оставаться чистыми.
  • Из статьи про middleware, hooks, guards берём принцип “сквозные политики отдельно”: формат ошибок валидации, request-id и политика доступа не должны размазываться по обработчикам.
  • Полезные источники:

  • Документация Litestar
  • Спецификация OpenAPI
  • Документация Pydantic
  • Документация msgspec
  • Документация dataclasses в Python
  • !Поток данных: от запроса через DTO к handler и обратно, плюс генерация OpenAPI

    Что именно мы валидируем и сериализуем в Litestar

    На практике Litestar извлекает данные из нескольких источников и преобразует их в типы Python:

  • Path параметры: /{user_id:int}
  • Query параметры: ?limit=10&offset=0
  • Headers: X-Request-ID, Authorization
  • Body: JSON, form-data и другие типы тел
  • Валидация решает задачи:

  • остановить неверный запрос как можно раньше
  • выдать предсказуемую ошибку клиенту
  • получить на вход обработчика типизированные данные
  • Сериализация решает задачи:

  • вернуть клиенту только разрешённые поля
  • обеспечить стабильный формат ответа
  • согласовать формат с документацией
  • DTO в Litestar: транспортные модели как контракт

    DTO в контексте Litestar удобно понимать как транспортное представление данных, которое может отличаться от доменной модели.

    Зачем это нужно в продвинутой архитектуре:

  • Безопасность: нельзя случайно вернуть password_hash, внутренние флаги, технические поля.
  • Совместимость: доменная модель может меняться, а DTO остаётся стабильной.
  • Контроль формата: имена полей, вложенность, исключения полей, преобразование типов.
  • Тестируемость: DTO проще тестировать как отдельный контракт.
  • Практическое правило: handler должен принимать DTO или типизированную команду, а доменный слой должен жить отдельно.

    Выбор базового типа моделей: dataclass, Pydantic, msgspec

    Litestar хорошо сочетается с несколькими подходами к моделям. Выбор влияет на скорость, строгую типизацию и удобство.

    | Подход | Когда выбирать | Сильные стороны | Ограничения | |---|---|---|---| | dataclass | простые схемы, минимум зависимостей | стандартная библиотека, понятность | валидация обычно проще и менее “богатая” | | Pydantic модели | богатая валидация, сложные правила | мощная экосистема, удобные ошибки | добавляет зависимость и накладные расходы | | msgspec структуры | производительность, строгие типы | быстрое кодирование/декодирование | нужна дисциплина типов, меньше “магии” |

    Важно: независимо от выбранного типа, вы должны стремиться к тому, чтобы DTO были отдельными типами, а не “прямым экспортом” ORM-модели.

    Входные DTO: как валидировать body и параметры

    Типизированные path и query параметры

    Litestar умеет валидировать path параметры прямо в маршруте и преобразовывать их к указанному типу.

    Для query параметров принцип тот же: Litestar пытается преобразовать значения к типам из сигнатуры.

    Практическое правило: параметры запроса должны быть типизированы так же строго, как и body.

    Body DTO на базе dataclass

    Пример “команды” на создание пользователя:

    Идея: обработчик получает структурированный объект вместо “сырого” dict.

    Явное применение DTO (dto и return_dto)

    В более крупных проектах удобно явно фиксировать транспортные правила через параметры маршрута.

    > Конкретные классы DTO и их конфигурация зависят от версии Litestar и выбранной системы моделей. Общая архитектурная идея неизменна: dto отвечает за вход, return_dto отвечает за выход.

    Концептуальный пример:

    Практическое правило: если у вас есть риск “утечки полей” или вы хотите гарантировать формат ответа, используйте return DTO как обязательный слой.

    Выходные DTO: контроль сериализации и защита от утечек

    Типовая проблема: доменная или ORM-модель содержит поля, которые нельзя показывать наружу.

    Один из безопасных паттернов:

  • доменная модель или модель БД содержит все поля
  • наружу возвращается отдельный UserPublicDTO
  • Это простая форма “return DTO”: вы физически не можете вернуть запрещённое поле, потому что его нет в типе.

    OpenAPI в Litestar: документация как побочный продукт типизации

    Litestar строит OpenAPI-спецификацию в основном из:

  • сигнатур обработчиков и их аннотаций типов
  • настроек маршрута (путь, методы)
  • метаданных, которые вы задаёте для документации
  • Практическая ценность OpenAPI в продвинутом проекте:

  • единый контракт для фронтенда и интеграций
  • автогенерация клиентов
  • тестирование контракта
  • упрощение ревью изменений API
  • Документационные метаданные маршрута

    Даже без глубоких настроек полезно добавлять summary, description, tags.

    Практическое правило: метаданные должны жить рядом с маршрутом, а не в внешних wiki.

    Конфигурация OpenAPI на уровне приложения

    OpenAPI настраивается централизованно на уровне Litestar(...). Конкретные классы конфигурации и плагины UI могут отличаться по версии, но сама архитектура стабильна.

    Концептуальный пример структуры:

    Практическое правило: OpenAPI конфигурация должна быть частью сборки приложения, как middleware и DI.

    Кастомные схемы и нестандартные типы

    В реальных API быстро появляются “сложные” поля:

  • datetime
  • Decimal
  • UUID
  • значения-сентинелы для частичных обновлений
  • доменные типы, которые неудобно сериализовать напрямую
  • Стратегия: не протаскивать нестандартные типы наружу

    Самый устойчивый вариант:

  • доменный слой хранит удобные для него типы
  • DTO слой преобразует их в переносимый формат
  • Пример: доменный слой использует Decimal, наружу отдаём строку.

    Так вы одновременно решаете две задачи:

  • сериализация становится предсказуемой
  • OpenAPI получает понятную схему (string, а не “неизвестный тип”)
  • Стратегия: централизованные правила сериализации

    Если вы используете Pydantic или msgspec, у них есть механизмы настройки сериализации и схем.

  • В Pydantic обычно используют конфигурацию модели и правила кодирования.
  • В msgspec обычно используют соответствующие структуры и энкодеры.
  • Практическое правило: если вы вводите кастомную сериализацию, она должна быть централизована, иначе API начнёт “плыть” от эндпоинта к эндпоинту.

    Как согласовать DTO, DI и безопасность

    DTO обычно зависят от:

  • формата внешнего API
  • требований безопасности
  • А бизнес-логика зависит от:

  • сервисов
  • репозиториев
  • политик домена
  • Правильная стыковка выглядит так:

  • Guard проверяет доступ и кладёт нужный контекст в request.state или возвращает ошибку
  • DI выдаёт сервисы и инфраструктуру
  • Handler принимает DTO, вызывает доменный сервис
  • Handler возвращает доменный результат, но наружу отдаётся return DTO
  • Типовая ошибка: возвращать наружу ORM-модель “как есть” ради скорости разработки. Это почти всегда приводит к поломке контракта, утечкам полей и сложным миграциям API.

    Рекомендации по структуре проекта

  • DTO храните в отдельном модуле домена, например users/dto.py.
  • Доменные типы храните отдельно, например users/domain.py или users/models.py.
  • Обработчики должны быть тонкими: валидация и преобразование на входе, вызов сервиса, преобразование на выходе.
  • Метаданные OpenAPI задавайте рядом с обработчиком, а глобальную конфигурацию OpenAPI держите в точке сборки приложения.
  • Что дальше

    После того как вход и выход стандартизированы через DTO, а контракт документируется через OpenAPI, следующий шаг в продвинутом Litestar-приложении:

  • единый формат ошибок и обработка исключений
  • наблюдаемость: логирование, метрики, трассировка
  • контрактные тесты: проверка того, что OpenAPI и фактические ответы не расходятся
  • 5. Интеграция с БД: репозитории, транзакции, миграции, async-паттерны

    Интеграция с БД: репозитории, транзакции, миграции, async-паттерны

    Интеграция с базой данных в Litestar почти всегда становится центральной частью архитектуры. В продвинутом приложении важно не просто подключиться к БД, а сделать это так, чтобы:

  • доступ к данным был тестируемым и модульным (через репозитории и сервисы)
  • транзакции были корректными и предсказуемыми (через request-scoped зависимости и yield-providers)
  • схема БД развивалась управляемо (через миграции)
  • асинхронная модель исполнения ASGI не приводила к блокировкам и утечкам ресурсов
  • Связь с предыдущими статьями курса:

  • Из темы про архитектуру Litestar берём принцип композиции: HTTP-слой тонкий, логика живёт в сервисах, доступ к данным оформлен как отдельный слой.
  • Из темы про DI берём Provide, scopes и yield-providers: пул и движок создаются в lifecycle, сессия и транзакция живут в request scope.
  • Из темы про middleware/hooks/guards берём идею сквозных политик: request-id, логирование и авторизация не смешиваются с кодом репозитория.
  • Из темы про DTO и OpenAPI берём границу транспорта: в обработчики приходит DTO, в репозитории и домене живут свои типы.
  • Полезные источники:

  • Документация Litestar
  • Документация SQLAlchemy: AsyncIO
  • Документация asyncpg
  • Документация Alembic
  • !Схема уровней: handler → service → repository → session → pool и связь с lifecycle

    Выбор технологии доступа к данным

    В Litestar нет “единственного правильного” способа работы с БД. Обычно выбирают один из подходов:

  • Асинхронная ORM (часто SQLAlchemy Async ORM)
  • Асинхронный драйвер без ORM (например, asyncpg) и ручной SQL
  • Синхронная ORM/драйвер (возможна, но требует дисциплины, чтобы не блокировать event loop)
  • Критерии выбора:

  • Производительность и контроль SQL: ручной SQL и asyncpg дают максимум контроля.
  • Скорость разработки и сложные связи: ORM часто удобнее.
  • Требования к миграциям: в реальных проектах миграции нужны почти всегда.
  • Практическое правило: если вы не уверены, выбирайте стек Async SQLAlchemy + Alembic. Он хорошо документирован, широко используется и позволяет масштабировать архитектуру.

    Главный принцип: два уровня жизни ресурсов

    При интеграции с БД важно различать два типа объектов:

  • app-scoped ресурсы: создаются на startup, живут до shutdown
  • request-scoped ресурсы: создаются на запрос и гарантированно закрываются после ответа
  • Типовая раскладка:

  • app-scoped: engine/pool (пул соединений)
  • request-scoped: session/transaction (единица работы на один запрос)
  • Эта модель напрямую опирается на DI и lifecycle из предыдущей статьи: тяжёлые ресурсы не создаём в обработчиках.

    Lifecycle: создание и закрытие engine/pool

    Ниже пример на SQLAlchemy Async. Он демонстрирует ключевую идею: engine создаётся на старте и хранится в app.state, а в обработчики попадает через DI.

    Что здесь важно:

  • create_async_engine() создаёт объект engine, который управляет пулом соединений.
  • engine.dispose() корректно закрывает пул на shutdown.
  • provide_engine() делает доступ к ресурсу типизированным и централизованным.
  • Request scope: сессия и транзакция через yield-provider

    Для большинства API удобная модель такая:

  • на запрос создаём AsyncSession
  • внутри запроса используем её во всех репозиториях
  • в конце запроса делаем commit или rollback
  • всегда закрываем сессию
  • Это идеально ложится на yield-provider из темы про DI.

    Как это читать:

  • до yield создаём сессию
  • на yield “отдаём” её обработчику и всем зависимостям
  • после yield гарантированно завершаем транзакцию
  • commit выполняется только если не было исключений
  • Практическая рекомендация: почти всегда делайте “транзакция на запрос” для команд (создание/изменение). Для чистых чтений иногда выбирают режим без commit, но единый шаблон проще поддерживать.

    Репозитории: слой доступа к данным без фреймворка

    Репозиторий — это объект, который инкапсулирует запросы к БД и возвращает доменные сущности (или структуры), не зная про HTTP, DTO и OpenAPI.

    Ключевые свойства хорошего репозитория:

  • принимает session (или интерфейс работы с БД) как зависимость
  • не читает Request, не обращается к app.state
  • не выбрасывает “сырой” SQL/ORM наружу без необходимости
  • Пример репозитория (SQLAlchemy)

    Замечание: пример выше показывает форму, но в реальном проекте User обычно будет ORM-моделью или доменным типом, который вы собираете из ORM-модели.

    Provider для репозитория

    Главная идея: репозиторий получает уже готовую request-scoped сессию.

    Сервисы и unit of work: где должна жить бизнес-логика

    Если репозиторий отвечает за запросы к данным, то сервис отвечает за бизнес-правила:

  • проверки и инварианты
  • координацию нескольких репозиториев
  • доменные операции, которые могут менять несколько агрегатов
  • Часто используют паттерн unit of work как объект, который содержит репозитории и общую транзакцию (то есть одну session).

    Пример unit of work

    Тогда сервис получает uow и работает через него, не создавая репозиторий вручную и не думая о транзакции.

    Обработчики Litestar: тонкая граница и типизированный контракт

    На уровне handler’а мы соединяем:

  • входной DTO
  • сервисы через DI
  • выходной DTO
  • Здесь важна дисциплина слоёв:

  • handler не содержит SQL
  • handler не управляет транзакцией
  • handler не знает, где живёт engine
  • Async-паттерны и типовые ошибки

    Не блокируйте event loop

    ASGI-приложение обслуживает много запросов конкурентно. Если вы делаете синхронный I/O в async def, вы блокируете обработку других запросов.

    Типовые источники блокировок:

  • синхронные драйверы БД внутри async def
  • тяжёлая сериализация или CPU-bound операции без вынесения
  • Если вы вынуждены использовать синхронный доступ к БД:

  • выносите его в threadpool
  • или выбирайте полностью асинхронный стек
  • Практическое правило: смешивание sync и async допустимо, но должно быть явным и централизованным, иначе вы получите нестабильную производительность.

    Не храните request-данные в app-scoped объектах

    Нельзя класть в app.state ничего, что зависит от пользователя/запроса:

  • текущий пользователь
  • tenant
  • request-id
  • текущая сессия БД
  • Это приводит к утечкам контекста между запросами.

    Одна сессия на запрос

    В большинстве веб-приложений самая безопасная стратегия:

  • одна AsyncSession на запрос
  • кэширование зависимости на request scope, чтобы все репозитории получили одну и ту же сессию
  • Так вы избегаете сценария, где часть изменений в одном запросе была сделана в одной транзакции, а часть в другой.

    Изоляция транзакции

    Транзакция должна начинаться и завершаться в одном месте. Самые частые ошибки:

  • commit() внутри репозитория
  • commit() внутри сервиса в середине операции
  • смешивание нескольких сессий в одной бизнес-операции
  • Правило: репозитории и сервисы не коммитят, если вы выбрали модель “транзакция на запрос”. Коммитом управляет yield-provider.

    Миграции: как развивать схему БД

    Миграции нужны, чтобы изменения схемы были:

  • воспроизводимыми
  • ревьюируемыми
  • применимыми в CI/CD
  • Если вы используете SQLAlchemy, стандартная связка для миграций — Alembic.

    Минимальная организационная модель миграций

  • миграции хранятся в репозитории рядом с кодом
  • есть команда “создать миграцию” и команда “применить миграции”
  • миграции применяются до запуска приложения или на этапе деплоя
  • Практика деплоя

    Частая безопасная стратегия:

  • выкатить миграции
  • выкатить новую версию приложения
  • В обратном порядке можно сломать приложение, если оно ожидает новую схему.

    Тестирование миграций

    Проверяйте, что:

  • миграции применяются на пустой базе
  • миграции применяются на базе с предыдущей версией схемы
  • миграции обратимы (если ваша политика требует downgrade)
  • Чеклист продвинутой интеграции с БД

  • Engine/pool создаётся в on_startup и закрывается в on_shutdown.
  • Сессия создаётся request-scoped yield-provider’ом и всегда закрывается.
  • Коммит и роллбек централизованы в provider’е, а не размазаны по репозиториям.
  • Репозитории не зависят от Litestar и HTTP.
  • Сервисы содержат бизнес-логику и координируют репозитории.
  • DTO отделяют транспортный слой от домена и модели БД.
  • Миграции являются обязательной частью процесса изменений.
  • Что дальше по курсу

    После того как доступ к данным и транзакции стандартизированы, следующий шаг в зрелом Litestar-приложении:

  • единый формат ошибок и централизованные обработчики исключений
  • наблюдаемость: структурированное логирование, метрики, трассировка
  • тестирование слоёв: репозитории, сервисы, интеграционные тесты с тестовой БД
  • 6. Производительность и наблюдаемость: кеширование, логирование, метрики, трассировка

    Производительность и наблюдаемость: кеширование, логирование, метрики, трассировка

    Продвинутое Litestar-приложение должно не только правильно работать, но и предсказуемо работать под нагрузкой и объяснять своё поведение в продакшене. Это две связанные дисциплины:

  • Производительность отвечает за скорость и устойчивость: меньше лишних операций, меньше блокировок, меньше повторных вычислений.
  • Наблюдаемость отвечает за диагностику: почему запрос медленный, где ошибка, какова доля 5xx, что происходит с БД.
  • Связь с предыдущими темами курса:

  • Из темы про архитектуру берём композицию: политики применяются на уровне app, router, controller, handler.
  • Из темы про DI берём idea scopes и кэширование зависимостей: app-scoped ресурсы создаются в lifecycle, request-scoped живут в пределах запроса.
  • Из темы про middleware/hooks/guards берём место для сквозных механизмов: request-id, логирование, метрики и трассировка обычно реализуются как middleware и хуки.
  • Из темы про DTO и OpenAPI берём контрактность: корректные статусы/ошибки и стабильные поля облегчают мониторинг и алёртинг.
  • Полезные источники:

  • Документация Litestar
  • Документация Python logging
  • Structlog
  • OpenTelemetry
  • Prometheus
  • Grafana
  • !Диаграмма, показывающая где в Litestar размещаются кеширование, логирование, метрики и трассировка

    Производительность в Litestar: где обычно теряется время

    В типичном API основное время уходит не на Python-код, а на I/O и инфраструктуру:

  • обращения к БД
  • обращения к внешним HTTP-сервисам
  • сериализацию больших ответов
  • ожидание блокирующих операций внутри async def
  • Поэтому эффективные оптимизации почти всегда системные:

  • переиспользовать дорогие ресурсы (пулы, клиенты)
  • не делать лишнюю работу повторно (кеширование)
  • измерять и находить реальные узкие места (метрики и трассы)
  • Кеширование: уровни и архитектурные решения

    Кеширование бывает разным по смыслу. Важно отличать:

  • кеширование зависимостей внутри DI
  • кеширование вычислений в рамках запроса
  • кеширование ответов или доменных результатов между запросами (обычно Redis)
  • Кеширование в DI: правильное использование scopes

    Из статьи про DI у вас уже есть ключевая модель:

  • app-scoped зависимость создаётся редко и переиспользуется
  • request-scoped зависимость создаётся на запрос и очищается
  • Практические правила:

  • Пул БД, HTTP-клиент, конфиг приложения держите в app scope.
  • Сессию БД, транзакцию, текущего пользователя держите в request scope.
  • Не делайте request-specific объекты app-scoped, иначе получите утечку контекста между запросами.
  • Концептуальный пример регистрации зависимостей с разным временем жизни:

    Даже если provider вызывается часто, он возвращает один и тот же app-level ресурс.

    Request-local кеш: не Redis, а ускорение внутри одного запроса

    Иногда в рамках одного запроса нужно несколько раз получить одни и те же данные. Тогда полезен request-local кеш, который живёт в request.state.

    Пример идеи: если несколько слоёв в одном запросе будут читать tenant или user, можно вычислить один раз и сохранить:

    Это не заменяет внешний кеш, но снимает повторные вычисления внутри запроса.

    Межзапросное кеширование: Redis и контроль консистентности

    Кеш между запросами нужен, когда:

  • данные читаются часто и меняются редко
  • есть дорогой расчёт, который можно переиспользовать
  • вы хотите защитить БД от пиков чтения
  • Ключевые вопросы, которые нужно решить до внедрения:

  • ключ кеша: какие параметры запроса влияют на результат
  • TTL: как долго кеш считается валидным
  • инвалидация: что делать при изменении данных
  • изоляция: если есть multi-tenant, tenant обязан входить в ключ
  • Универсальный подход для API:

  • кешировать на уровне сервисов или репозиториев, а не в handler
  • хранить кеш-клиент в app.state и получать его через DI
  • логировать попадания и промахи кеша, иначе вы не поймёте эффект
  • Скелет зависимости для кеш-клиента (без привязки к конкретной библиотеке Redis):

    Анти-паттерны кеширования

  • Кешировать ответы без учёта авторизации или tenant, получая утечки данных.
  • Кешировать ошибки так же, как успешные ответы, и маскировать восстановление системы.
  • Кешировать бесконечно без TTL, получая вечную устаревшую правду.
  • Кешировать в глобальной переменной процесса, забывая, что у вас несколько воркеров.
  • Логирование: структурированность, корреляция, уровни

    Логи нужны не для того, чтобы печатать всё, а чтобы:

  • связывать события в рамках одного запроса
  • находить исключения и контекст
  • делать поиск и агрегацию в централизованном хранилище
  • Request-id как основа корреляции

    Из предыдущих тем у вас уже есть паттерн: request-id кладётся в request.state, а затем используется в логах.

    Практическая цель:

  • один запрос должен иметь один стабильный идентификатор
  • этот идентификатор должен присутствовать в логах, метриках и трассах
  • Где реализовать:

  • middleware или ранний hook
  • добавление X-Request-ID в ответ
  • Структурированные логи вместо строк

    Структурированный лог — это запись с полями, а не строка, например:

  • event: что случилось
  • request_id: идентификатор запроса
  • path, method, status_code
  • duration_ms
  • Для этого часто используют structlog поверх стандартного logging.

    Пример идеи (упрощённо):

    Важно: реальный формат полей зависит от вашего лог-пайплайна, но принцип одинаковый.

    Логирование в middleware: один раз на запрос

    Технически удобно логировать начало и конец запроса в middleware:

  • на входе фиксировать method, path, request_id
  • на выходе фиксировать status_code и длительность
  • на исключениях логировать stacktrace и тот же request_id
  • Такая архитектура лучше, чем логировать вручную внутри каждого handler.

    Что не стоит логировать

  • пароли, токены, секреты
  • полный body запросов/ответов по умолчанию
  • персональные данные без маскирования
  • Если вам нужен аудит, делайте его как отдельную подсистему, а не как побочный эффект от debug-логов.

    Метрики: измерение поведения системы

    Метрики отвечают на вопрос: как система ведёт себя в целом, а не что случилось в одном запросе.

    Типовые метрики API:

  • количество запросов по маршрутам и статусам
  • распределение длительностей (latency)
  • количество ошибок по типам
  • насыщение пула соединений (если снимается с БД-клиента)
  • Где снимать метрики в Litestar

    Базовые HTTP-метрики логично снимать в middleware, потому что middleware видит:

  • факт запроса
  • факт ответа или исключения
  • длительность вокруг всего обработчика
  • Практическая рекомендация по labels:

  • используйте шаблон маршрута вместо сырого пути
  • не используйте user_id или любые высококардинальные значения в метках
  • Пояснение:

  • метка path=/users/123 создаст тысячи уникальных рядов
  • метка route=/users/{user_id:int} останется стабильной
  • Prometheus как стандартный формат экспорта

    Часто метрики экспортируют в Prometheus-формате и визуализируют в Grafana.

    Высокоуровневый план:

  • middleware пишет счётчики и тайминги
  • отдельный эндпоинт отдаёт метрики
  • Prometheus опрашивает этот эндпоинт
  • Конкретная интеграция зависит от выбранной библиотеки, поэтому архитектурно важно помнить только следующее:

  • метрики должны быть централизованы
  • метки должны быть контролируемы
  • метрики должны быть частью сборки приложения, как middleware
  • Трассировка: ответы на вопрос «где именно было медленно»

    Логи и метрики показывают симптомы, но не всегда показывают причину. Трассировка отвечает на вопрос:

  • сколько времени занял запрос
  • какие внутренние операции были внутри
  • где была задержка: БД, внешний сервис, CPU, очередь
  • Трассировка обычно строится как дерево:

  • trace: один запрос или одна операция
  • span: шаг внутри операции (например, SQL-запрос)
  • OpenTelemetry как экосистема

    OpenTelemetry даёт единый стандарт для:

  • traces
  • metrics
  • logs (в зависимости от стека)
  • Практический подход:

  • включить ASGI-инструментацию, чтобы создавать span на каждый запрос
  • добавить собственные spans вокруг критичных операций
  • прокидывать trace_id в логи, чтобы связывать traces и logs
  • Документация:

  • OpenTelemetry
  • Где в Litestar добавлять spans

    Варианты размещения:

  • middleware трассировки оборачивает весь запрос
  • сервисы создают span вокруг внешних вызовов
  • репозитории и ORM-инструментация создают spans вокруг запросов к БД
  • Главный принцип: handler остаётся тонким, а наблюдаемость живёт как сквозная политика.

    Единая стратегия: связать логи, метрики и трассы одним контекстом

    Наблюдаемость становится значительно сильнее, когда всё связано:

  • request-id в request.state и заголовках
  • trace_id в spans и в логах
  • метки метрик отражают маршрут и статус
  • Рекомендуемая практика для Litestar-приложения:

  • middleware RequestIdMiddleware отвечает за request_id
  • middleware ObservabilityMiddleware отвечает за тайминги, статус, базовые метрики и обогащение логов
  • DI выдаёт ресурсы (БД, кеш, внешние клиенты) и не создаёт их на каждый запрос
  • Практический чеклист

    Производительность

  • Тяжёлые клиенты создаются на startup, закрываются на shutdown.
  • Никаких блокирующих операций внутри async def.
  • Кеширование внедряется там, где есть измеримая польза.
  • Логирование

  • Структурированные логи с request_id.
  • Единое логирование входа/выхода запроса в middleware.
  • Нет секретов и персональных данных в логах.
  • Метрики

  • HTTP-метрики с контролируемыми метками.
  • Нет высококардинальных labels.
  • Экспорт метрик интегрирован в приложение.
  • Трассировка

  • Автоспаны на запросы через ASGI-инструментацию.
  • Спаны вокруг внешних вызовов и БД.
  • Связка trace_id и логов.
  • Что дальше по курсу

    Теперь у вас есть инструменты, чтобы:

  • ускорять систему на основе измерений
  • объяснять поведение системы в продакшене
  • делать алёрты на реальные деградации
  • Следующий логический шаг зрелой архитектуры Litestar:

  • централизованная обработка ошибок и единый формат ошибок
  • интеграционные тесты наблюдаемости (проверка, что request-id и trace действительно появляются)
  • продакшн-конфигурация: уровни логов, безопасные значения меток, настройка экспорта метрик и трасс
  • 7. Тестирование и production: contract tests, конфигурация, деплой, CI/CD

    Тестирование и production: contract tests, конфигурация, деплой, CI/CD

    Litestar помогает строить архитектурно явные ASGI-сервисы: маршрутизация и контроллеры задают структуру, DI и lifecycle управляют ресурсами, middleware и guards задают сквозные политики, DTO и OpenAPI фиксируют контракт, а слой БД задаёт границы транзакций.

    Чтобы эта архитектура была живучей в реальном продакшене, нужны четыре практики:

  • Тестирование на разных уровнях, чтобы изменения были безопасными.
  • Contract tests, чтобы реализация не расходилась с OpenAPI-контрактом.
  • Конфигурация, чтобы один и тот же код корректно работал в dev, test и prod.
  • Деплой и CI/CD, чтобы изменения доставлялись повторяемо, с проверками и откатами.
  • !Диаграмма типового CI/CD потока для Litestar

    Тестовая стратегия: что именно мы проверяем

    Хорошая стратегия тестирования следует границам из предыдущих тем курса: обработчики тонкие, сервисы содержат бизнес-логику, репозитории инкапсулируют доступ к данным, ресурсы создаются в lifecycle и выдаются через DI.

    Уровни тестов

    | Уровень | Что тестирует | Где живёт логика | Типичные зависимости | Что должно быть быстрым | |---|---|---|---|---| | Unit | Бизнес-правила и преобразования данных | Сервисы, доменные функции | Фейки репозиториев и клиентов | Да | | Integration | Интеграцию с инфраструктурой | Репозитории, транзакции, миграции | Реальная БД, Redis, внешние клиенты как мок | Умеренно | | HTTP-level | Сборку приложения и сквозные политики | Middleware, guards, DI-граф | TestClient и конфиг тестового app | Да | | Contract | Соответствие контракту OpenAPI | Внешний API как поведение | OpenAPI-спека и запущенный сервис | Умеренно |

    Практическое правило: максимум логики проверяйте unit-тестами, а минимально необходимое количество сценариев закрывайте integration и contract тестами.

    Инструменты: минимальный стек

  • pytest как тестовый раннер.
  • pytest-asyncio для async тестов.
  • HTTPX как HTTP-клиент, если вы тестируете сервис как внешнюю систему.
  • Встроенный тестовый клиент Litestar для HTTP-level тестов.
  • Вы можете расширить стек под вашу инфраструктуру:

  • testcontainers-python для поднятия PostgreSQL/Redis в тестах.
  • Schemathesis для contract tests по OpenAPI.
  • Фабрика приложения: основа тестируемости

    Ключевой приём для тестов и production одновременно: собирать приложение через фабрику, которая принимает настройки и зависимости.

    Почему это важно именно для Litestar:

  • DI-слой становится управляемым: вы можете подменять providers на фейки.
  • Lifecycle становится проверяемым: startup/shutdown можно выполнять в тестах.
  • Конфигурация окружений становится явной: тесты не “попадают” в продакшн ресурсы.
  • Пример упрощённой фабрики:

    Идея: тест создаёт Settings(app_name="test"), продакшн создаёт Settings(app_name="service"), а код обработчиков и DI-граф остаются одинаковыми.

    Подмена зависимостей в тестах: фейки вместо инфраструктуры

    Связь с темой про DI: правильно спроектированные providers зависят от настроек и интерфейсов, поэтому их легко заменить.

    Пример теста, который подменяет внешний клиент на фейк:

    Что здесь проверяется:

  • маршрутизация и парсинг query параметров
  • DI и подмена зависимости
  • сериализация ответа
  • Что здесь не проверяется:

  • реальный HTTP вызов во внешний сервис
  • Это правильное разделение: внешний I/O проверяется отдельными integration тестами.

    Integration тесты для БД: проверяем транзакции и миграции

    Связь с темой про БД: если вы используете yield-provider для сессии и транзакции, то integration тесты должны подтверждать две вещи:

  • транзакция корректно коммитится при успехе
  • транзакция корректно откатывается при исключении
  • Рекомендуемая практика:

  • Поднимать тестовую БД в контейнере на время набора тестов.
  • Прогонять миграции перед запуском тестов.
  • Не использовать одну и ту же базу для параллельных тестов без изоляции.
  • Инструменты:

  • Alembic из темы про миграции: Alembic
  • Контейнеры: testcontainers-python
  • Contract tests: защита от расхождения с OpenAPI

    Contract test проверяет: если OpenAPI обещает такое поведение, то сервис действительно так отвечает.

    Это особенно важно для Litestar, потому что OpenAPI у вас появляется почти бесплатно из аннотаций типов и DTO. Риск в том, что со временем:

  • вы меняете DTO или код, и поведение меняется
  • OpenAPI остаётся прежней или меняется не там, где нужно
  • Contract tests дают автоматическую страховку.

    Что именно проверяют contract tests

  • что обязательные поля действительно обязательны
  • что типы и форматы данных соответствуют контракту
  • что коды ответов и схемы ошибок не “плывут”
  • Schemathesis как практический инструмент

    Schemathesis умеет генерировать запросы на основе OpenAPI и фуззить входные данные, находя расхождения.

    Типичный рабочий процесс:

  • В CI поднимаете сервис (или запускаете его в тестовом режиме).
  • Получаете OpenAPI-спеку из вашего артефакта или по HTTP.
  • Запускаете Schemathesis против базового URL.
  • Пример командного запуска, когда у вас есть файл openapi.json:

    Практическое правило: contract tests должны быть частью CI и блокировать мердж, если контракт нарушен.

    Конфигурация: один код, разные окружения

    Конфигурация в продакшене решает три задачи:

  • Переключение окружений: dev, test, prod.
  • Безопасность: секреты не должны попадать в репозиторий и логи.
  • Повторяемость: одинаковые значения дают одинаковое поведение.
  • Хорошая отправная точка: принципы The Twelve-Factor App.

    Как хранить настройки

    Практическая архитектура (связь с темой про DI и lifecycle):

  • Настройки читаются на старте процесса.
  • Валидируются и превращаются в типизированный объект Settings.
  • Сохраняются в app.state.
  • Выдаются в обработчики и сервисы через DI.
  • Для валидации окружения часто используют pydantic-settings.

    Пример типизированных настроек через стандартный dataclass:

    Где держать секреты

  • В переменных окружения, которые поставляет платформа деплоя.
  • В secret manager вашей платформы, который монтирует секреты в env или файлы.
  • Запрещённые практики:

  • хранить секреты в git
  • логировать секреты
  • прокидывать секреты в OpenAPI или ответы API
  • Production runtime: ASGI сервер, graceful shutdown, health checks

    Litestar как ASGI-приложение запускают через ASGI-сервер.

  • Uvicorn как базовый ASGI сервер.
  • Gunicorn часто используется как процесс-менеджер с воркерами.
  • Graceful shutdown

    Связь с lifecycle: если вы создаёте пул БД и клиентов на on_startup, вы обязаны корректно закрыть их на on_shutdown, чтобы:

  • не терять данные из буферов
  • не рвать соединения некорректно
  • не получать “зависшие” воркеры при деплое
  • Health и readiness

    Рекомендуется иметь как минимум:

  • GET /health для liveness: процесс жив.
  • GET /ready для readiness: сервис готов обслуживать трафик, включая зависимости.
  • Важно: readiness часто должен проверять доступность ключевых зависимостей (БД, кеш), но делать это без тяжёлых операций.

    Деплой: контейнеры, миграции, откаты

    Docker как повторяемая упаковка

    Цель контейнера:

  • одинаковая среда локально, в CI и в продакшене
  • контролируемая команда запуска
  • Обычно в команду запуска включают:

  • применение миграций
  • старт ASGI-сервера
  • Практическое правило: миграции должны применяться до того, как новый код начнёт принимать трафик.

    Стратегии выката

    Выбор зависит от платформы, но базовые понятия стоит знать:

  • Rolling update: обновление по частям без остановки сервиса.
  • Blue-green: две версии живут параллельно, трафик переключается.
  • Canary: новая версия получает малую долю трафика.
  • Общее требование для всех стратегий: метрики, логи и трассы должны показывать деградацию быстро, иначе вы не успеете откатиться.

    CI/CD: минимально полезный пайплайн

    Цель CI: не сломать качество.

    Цель CD: доставить изменения повторяемо.

    Типичный пайплайн (можно реализовать в GitHub Actions):

  • Форматирование и линтинг.
  • Проверка типов.
  • Unit тесты.
  • Integration тесты с тестовой инфраструктурой.
  • Contract tests по OpenAPI.
  • Сборка артефакта (обычно Docker-образ) и публикация.
  • Деплой в окружение.
  • Smoke тесты и проверки health/readiness.
  • Практическое правило: если шаг можно сделать автоматическим и детерминированным, его надо делать в CI, а не “по инструкции в README”.

    Итоговый чеклист production-готовности Litestar сервиса

  • Архитектура
  • - обработчики тонкие, логика в сервисах - репозитории не зависят от Litestar
  • DI и lifecycle
  • - app-scoped ресурсы создаются на startup и закрываются на shutdown - request-scoped ресурсы имеют корректный cleanup через yield
  • Тестирование
  • - unit тесты покрывают бизнес-правила - integration тесты покрывают БД и транзакции - contract tests блокируют расхождения с OpenAPI
  • Конфигурация
  • - настройки типизированы - секреты вне репозитория и вне логов
  • Деплой
  • - миграции применяются до переключения трафика - есть health и readiness эндпоинты
  • CI/CD
  • - проверки выполняются автоматически - деплой воспроизводим и наблюдаем