Основной принцип — не тестируем то, что уже тестировали, и не тестируем очевидную логику. По умолчанию используется функциональное (BDD) тестирование. Unit-тесты пишутся только для логики, которую невозможно или неэффективно проверить через BDD-сценарии.
Данный документ описывает единые стандарты использования structlog в Python-проектах.
Соблюдение правил обеспечивает консистентность JSON-логов, удобство поиска в ELK/Grafana Loki и чистоту кода.
Документ описывает стандарты разработки с использованием фреймворка FastStream. Основной фокус — надежность обработки сообщений, идемпотентность, чистая архитектура, правильная работа с контекстом и операционная устойчивость.
Документ описывает стандарты разработки API на FastAPI. Основной фокус — производительность (async), чистая архитектура (Layered Architecture) и безопасность типов.
title: Чек-лист для Fastapi
1. Архитектура: Слоистая структура (Layered Architecture)
FastAPI не навязывает структуру, поэтому важно самостоятельно разделять ответственности. Не размещайте бизнес-логику в обработчиках (endpoint functions).
Бизнес-логика, транзакции, вызов внешних API, работа с БД через Repository.
Repository/DAO
Запросы к БД (SQLAlchemy, Tortoise ORM), построение запросов.
Schemas (Pydantic)
Контракт данных (DTO), валидация полей, сериализация.
Правильно
# users/router.py
@router.post("/users",response_model=UserOut,status_code=201)asyncdefcreate_user(payload:UserCreate,service:UserService=Depends(get_user_service)):returnawaitservice.create_user(payload)# users/service.py
classUserService:def__init__(self,db:AsyncSession,cache:Redis):self.db=dbself.cache=cacheasyncdefcreate_user(self,payload:UserCreate)->UserOut:# Логика здесь, а не в роутере
ifawaitself.check_email_exists(payload.email):raiseEmailAlreadyExistsError()user=User(email=payload.email,hashed_password=hash(payload.password))self.db.add(user)awaitself.db.commit()awaitself.db.refresh(user)# Явный маппинг на границе слоя сервиса (безопасность + избежание lazy loading)
returnUserOut.model_validate(user)
Неправильно
# users/router.py
@router.post("/users")asyncdefcreate_user(payload:UserCreate,db:AsyncSession=Depends(get_db)):# Вся логика в роутере — плохо
ifawaitdb.execute(select(User).where(User.email==payload.email)):raiseHTTPException(400,"Exists")user=User(**payload.dict())db.add(user)awaitdb.commit()returnuser
2. Dependency Injection: Система зависимостей
DI в FastAPI — мощный инструмент для управления ресурсами (БД, кэш, сервисы).
Управление ресурсами (Context Managers)
Используйте yield для зависимостей, требующих закрытия (сессии БД, файлы).
Правильно
# deps/db.py
asyncdefget_db():asyncwithasync_session_maker()assession:try:yieldsessionfinally:# Закрытие происходит автоматически
pass# deps/services.py
defget_user_service(db:AsyncSession=Depends(get_db))->UserService:returnUserService(db)
Важно: Жизненный цикл
Depends() по умолчанию кеширует результат в рамках одного HTTP запроса. Это оптимально и позволяет безопасно вызывать одну и ту же зависимость несколько раз.
Используйте yieldтолько для ресурсов, требующих закрытия (БД, HTTP-клиенты, файловые дескрипторы).
Анти-паттерн: Создание сервиса через yield без finally — это избыточно и может удерживать память дольше необходимого.
3. Асинхронность: Запрещенные блокирующие вызовы
FastAPI работает в одном потоке event-loop. Блокирующий вызов останавливает всё приложение.
Выбор сигнатуры: def vs async def
Используйте async def, если внутри функции есть хотя бы один await.
Используйте def (синхронную), если внутри нет асинхронных вызовов. FastAPI автоматически запустит её в threadpool, не нагружая event-loop лишними обертками.
Таблица блокирующих операций
Операция
Блокирующее
Неблокирующее
HTTP запрос
requests.get()
httpx.get() / aiohttp
БД (Postgres)
psycopg2, sqlite3
asyncpg, SQLAlchemy (async), databases
Время
time.sleep()
asyncio.sleep()
Файловая система
open(), os.path
aiofiles, anyio
Как быть, если нужна синхронная библиотека?
Если библиотеку нельзя заменить на асинхронную, используйте run_in_threadpool.
fromfastapi.concurrencyimportrun_in_threadpool@router.get("/sync-op")asyncdefdo_sync():# Выполняется в отдельном пуле потоков, не блочит event-loop
result=awaitrun_in_threadpool(heavy_sync_function,arg1,arg2)returnresult
4. Pydantic: Валидация и Сериализация
Pydantic v2 требует явного разделения моделей и поддерживает современные подходы к типизации.
Группы моделей (Schema Design)
Не используйте одну модель User из ORM везде. Создавайте отдельные схемы:
UserCreate — входные данные (пароль, email).
UserOut — выходные данные (id, email, но БЕЗ пароля).
UserUpdate — частичное обновление (все поля Optional).
Использование response_model
Всегда указывайте response_model. Это фильтрует выходные данные (например, скрывает password_hash) и генерирует OpenAPI схему.
Правильно (с использованием Annotated для переиспользования)
fromtypingimportAnnotatedfrompydanticimportBaseModel,ConfigDict,EmailStr,FieldclassUserCreate(BaseModel):email:Annotated[EmailStr,Field(description="User email address")]password:Annotated[str,Field(min_length=8,description="User password")]classUserOut(BaseModel):id:intemail:EmailStris_active:bool=Truemodel_config=ConfigDict(from_attributes=True)# Для маппинга из ORM
# router.py
@router.post("/users",response_model=UserOut)asyncdefcreate_user(data:UserCreate):...
5. Обработка ошибок
Не используйте HTTPException внутри слоя сервисов. Сервисы должны выбрасывать доменные исключения, которые перехватываются глобальным обработчиком.
Используйте pydantic-settings для управления конфигурацией. Не используйте .env файлы напрямую через os.getenv в разных местах.
Правильно
frompydantic_settingsimportBaseSettings,SettingsConfigDictclassSettings(BaseSettings):app_name:str="Awesome API"admin_email:strdatabase_url:strredis_url:strmodel_config=SettingsConfigDict(env_file=".env",env_file_encoding="utf-8")# Зависимость для настроек
defget_settings()->Settings:returnSettings()@router.get("/info")asyncdefinfo(settings:Settings=Depends(get_settings)):return{"app_name":settings.app_name}
Чек-лист для Code Review (FastAPI)
Перед мержем проверьте:
Разделение ответственности: В роутерах нет SQL-запросов и бизнес-логики (только вызов сервисов).
Асинхронность: Отсутствуют блокирующие вызовы (requests, time.sleep, psycopg2) внутри async def.
DI: Зависимости (БД, конфиг, сервисы) внедряются через Depends, а не создаются внутри функции.
Pydantic v2: Используется model_config = ConfigDict(...) вместо устаревшего class Config, применяется Annotated для переиспользуемых полей.
Безопасность:response_model исключает утечку чувствительных полей (пароли, токены), а сервис возвращает Pydantic-схему, а не ORM-объект.
Обработка ошибок: Используются кастомные исключения и exception_handler, а не множество try/except в роутерах.
Типизация: Функции имеют возвращаемые типы (return type hints).
Тестирование: Используется TestClient (для синхронных тестов) или httpx.AsyncClient (для асинхронных), БД мокируется или использует тестовую in-memory базу.