🧙 Садовник: уточнил шаги, добавил проверки и обоснования. Примите, если полезно.

#1
open+1119proposed by gardener · Aug 4, 2026 · based on v1
Result · becomes v2
1
Создай каркас папок

Внутри app/ создай пять прикладных слоёв и общее ядро. Пока в них не будет прикладного кода.

code
1app/
2├── models/ таблицы базы
3├── schemas/ контракты данных
4├── repository/ доступ к данным
5├── services/ бизнес-логика
6├── routers/ HTTP
7└── core/ общее для всех

Команда использует Python и одинаково работает в PowerShell, macOS и Linux при установленном uv.

Why: Пустое дерево — это решение, принятое заранее: когда появится первый файл, вопроса «куда его положить» уже не будет.
code
1uv run python -c "from pathlib import Path; [Path('app', name).mkdir(parents=True, exist_ok=True) for name in ('models', 'schemas', 'repository', 'services', 'routers', 'core')]"
  • Внутри app созданы ровно шесть целевых папок
  • Названия папок совпадают со схемой
  • В папках пока нет прикладного кода
2
Добавь __init__.py в каждую папку

Создай пустой __init__.py в самой папке app/ и во всех шести вложенных папках. Это делает их обычными Python-пакетами и обеспечивает предсказуемое обнаружение модулей инструментами сборки и анализа.

Why: Python поддерживает пакеты без `__init__.py`, но обычные пакеты надёжнее распознаются сборщиками, анализаторами и другими инструментами проекта.
code
1uv run python -c "from pathlib import Path; [p.joinpath('__init__.py').touch(exist_ok=True) for p in [Path('app'), *[x for x in Path('app').iterdir() if x.is_dir()]]]"
  • В каждой целевой папке есть __init__.py
  • Файл app/__init__.py тоже существует
  • Команда uv run python -c "import app" завершается без ошибки
3
Определи роль models

Здесь находятся классы SQLAlchemy: описание таблиц, колонок, связей, индексов и ограничений.

python
1class Category(BaseModel):
2 __tablename__ = "categories"
3
4 name: Mapped[str]
5 slug: Mapped[str] = mapped_column(unique=True, index=True)

Здесь не должно быть кодов HTTP-ответа, проверок прав, вызовов внешних сервисов и отправки писем.

Why: Модель описывает хранение и больше ничего: привязка к HTTP затруднит её использование в фоновой задаче или скрипте, а изменение API начнёт затрагивать слой базы данных.
  • Можешь объяснить, почему модель не должна знать про HTTP
  • Можешь отличить модель SQLAlchemy от схемы Pydantic
  • Проверил, что модели не импортируют routers и services
4
Определи роль schemas

Здесь находятся классы Pydantic, описывающие контракты входных и выходных данных. Для создания и ответа используй отдельные схемы.

python
1class CategoryCreate(BaseSchema):
2 name: str
3 parent_id: int | None = None
4
5class CategoryResponse(BaseSchema):
6 id: int
7 name: str
8 slug: str

В CategoryCreate нет id и slug, потому что их назначает сервер. Здесь не должно быть запросов к базе и бизнес-правил.

Why: Раздельные схемы не позволяют случайно принять или отдать поля, которые клиент не должен изменять либо видеть.
  • Можешь объяснить, зачем нужны разные классы на вход и выход
  • Понимаешь, почему id и slug отсутствуют в схеме создания
  • Проверил, что схемы не обращаются к базе данных
5
Определи роль repository

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

python
1async def get_by_slug(self, slug: str) -> Category | None:
2 stmt = select(Category).where(Category.slug == slug)
3 return await self.session.scalar(stmt)

Репозиторий возвращает None, если запись не найдена. Решать, является ли это ошибкой, должен сервис. Здесь не должно быть правил вроде «нельзя удалять категорию с товарами».

Why: Когда весь SQL сосредоточен в одном слое, запросы проще находить, тестировать и оптимизировать.
  • Можешь объяснить, почему репозиторий возвращает None
  • Проверил, что репозиторий не создаёт HTTPException
  • Проверил, что SQL не размещён в routers или services
6
Определи роль services

Здесь находятся правила предметной области. Например: slug генерируется из названия, категорию с потомками нельзя удалить, а имя должно быть уникально внутри родителя.

python
1async def delete(self, category_id: int) -> None:
2 if await self.repo.has_children(category_id):
3 raise ConflictError("У категории есть подкатегории")
4 await self.repo.delete(category_id)

Сервис вызывает репозиторий и выбрасывает собственные исключения, а не HTTPException. Здесь не должно быть кодов ответа, объекта HTTP-запроса и прямых SQL-запросов.

Why: Одни и те же правила смогут вызывать HTTP-маршрут, фоновая задача, импорт данных или консольная команда без дублирования логики.
  • Можешь назвать три правила, относящиеся к сервису
  • Понимаешь, почему сервис не выбрасывает HTTPException
  • Проверил, что сервис обращается к базе только через repository
7
Определи роль routers

Здесь находятся маршруты, HTTP-коды и модели ответа. Роутер должен быть тонким: принять данные, вызвать сервис и вернуть результат.

python
1@router.post("", status_code=201, response_model=CategoryResponse)
2async def create_category(
3 data: CategoryCreate,
4 service: CategoryServiceDep,
5) -> Category:
6 return await service.create(data)

Условие в роутере — повод проверить, не является ли оно бизнес-правилом. Здесь не должно быть SQL, предметных правил и локальных try/except, если ошибки уже преобразуются централизованными обработчиками.

Why: Тонкий роутер легко читать и тестировать, а вынесенная в сервис логика остаётся доступной без запуска HTTP-сервера.
  • Можешь объяснить, почему if в роутере требует проверки
  • Проверил, что роутер не обращается к базе напрямую
  • Проверил, что роутер вызывает сервис, а не repository
8
Определи роль core

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

В будущем структура может выглядеть так:

code
1core/
2├── settings/ конфигурация из окружения
3├── exceptions/ общие исключения и обработчики
4├── dependencies/ общие фабрики зависимостей
5└── security/ пароли и токены

Не создавай эти подпапки заранее без необходимости. Если содержимое core нужно только одному слою, перенеси его в этот слой.

Why: Общее ядро предотвращает циклические зависимости, но без строгого критерия быстро превращается в свалку несвязанных утилит.
  • Можешь назвать критерий попадания кода в core
  • Проверил, что core не импортирует routers или services
  • Не создавал вложенные папки без реальной необходимости
9
Зафиксируй правило зависимостей

Разреши зависимостям идти только от внешних деталей к внутренним: routers → services → repository → models. Схемы используются на границе данных, а core предоставляет общие механизмы и не должен зависеть от прикладных слоёв.

Обратный путь результата не означает обратного импорта: repository возвращает данные сервису, а сервис — роутеру, но нижние слои по-прежнему ничего не знают о верхних.

Why: Однонаправленные импорты предотвращают циклы и позволяют заменять HTTP, базу данных или способ запуска без переписывания бизнес-правил.
  • models не импортирует repository, services или routers
  • repository не импортирует services или routers
  • services не импортирует routers
  • core не импортирует прикладные слои
10
Нарисуй путь запроса на бумаге

Без подглядывания нарисуй цепочку для запроса «создать категорию»: какие слои вызываются, какие данные передаются вперёд и что возвращается обратно. Отдельно отметь направление импортов, чтобы не смешивать его с обратным движением результата.

Why: Рисунок быстро показывает, действительно ли понятны границы слоёв и отличие потока выполнения от направления зависимостей.
  • Цепочка запроса нарисована по памяти
  • Вызовы идут от routers к services и repository
  • Направление импортов не нарушает правило зависимостей
  • Результат возвращается обратно без обратных импортов
11
Опиши структуру в README

Добавь раздел «Структура проекта» и опиши каждую папку одной строкой своими словами. Отдельно запиши правило допустимого направления зависимостей.

Why: Собственная формулировка закрепляет понимание и помогает любому, кто впервые откроет проект, быстро найти нужный слой.
  • В README есть раздел «Структура проекта»
  • Все шесть папок описаны одной строкой
  • Указана цепочка routers → services → repository → models
  • Формулировки не скопированы дословно