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

#1
open+1119proposed by gardener · Aug 4, 2026 · based on v1
Proposed changes · v1 → suggestion
+1119
Создай каркас папокAdded

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

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

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

uv 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')]"
Добавь __init__.py в каждую папкуAdded

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

uv 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()]]]"
Определи роль modelsAdded

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

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

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

Определи роль schemasAdded

Здесь находятся классы 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, потому что их назначает сервер. Здесь не должно быть запросов к базе и бизнес-правил.

Определи роль repositoryAdded

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

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, если запись не найдена. Решать, является ли это ошибкой, должен сервис. Здесь не должно быть правил вроде «нельзя удалять категорию с товарами».

Определи роль servicesAdded

Здесь находятся правила предметной области. Например: 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-запросов.

Определи роль routersAdded

Здесь находятся маршруты, 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, если ошибки уже преобразуются централизованными обработчиками.

Определи роль coreAdded

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

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

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

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

Зафиксируй правило зависимостейAdded

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

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

Нарисуй путь запроса на бумагеAdded

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

Опиши структуру в READMEAdded

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

## Зачем заниматься структурой до кодаRemoved
## Зачем заниматься структурой до кода Когда файлов десять, структура кажется формальностью. Когда их девятьсот — она единственное, что позволяет найти нужное место за секунды. Задать её сейчас стоит пятнадцать минут. Перекладывать потом сотни файлов, починяя импорты, — день работы и гарантированные ошибки. **Важно сразу:** слоистая структура — не единственный вариант и не «правильный по определению». Есть модульная, гексагональная, плоская и другие — им посвящён отдельный список. Здесь мы берём слоистую, потому что на ней проще всего понять саму идею разделения ответственности.
## Путь одного запросаRemoved
## Путь одного запроса Вся структура существует ради этой цепочки. Запомни её — дальше всё будет про неё: ``` HTTP-запрос ↓ routers/ «пришёл POST /categories» ↓ schemas/ «что прислали — проверено и разобрано» ↓ services/ «можно ли так? что по правилам надо сделать?» ↓ repository/ «сходи в базу и принеси» ↓ models/ «так это лежит в таблицах» ↓ база данных ``` И обратно тем же путём. Каждый слой говорит **только со следующим**. Роутер не знает про базу, модель не знает про HTTP.
Создать каркас папокRemoved
Внутри `app/` создай шесть папок. Пока пустых — кода не будет ни строчки. ``` app/ ├── models/ таблицы базы ├── schemas/ контракт API ├── repository/ доступ к данным ├── services/ бизнес-логика ├── routers/ HTTP └── core/ общее для всех ``` На Windows в PowerShell можно одной строкой, на macOS и в Git Bash — командой ниже.
Положить __init__.py в каждую папкуRemoved
Файл пустой — ему не нужно содержимое, важен сам факт его существования. Не забудь про сам `app/__init__.py` — uv его уже создал, проверь, что он на месте.
## Теперь разберём каждую папкуRemoved
## Теперь разберём каждую папку Дальше шаги не про команды, а про понимание. На каждом ответь себе на вопрос из проверок — если не получается, перечитай раздел «зачем», а не иди дальше.
models — как данные лежат в базеRemoved
**Что здесь:** классы SQLAlchemy — описание таблиц, колонок, связей, индексов и ограничений. ```python class Category(BaseModel): __tablename__ = "categories" name: Mapped[str] slug: Mapped[str] = mapped_column(unique=True, index=True) ``` **Чего здесь быть не должно:** кодов ответа, проверок прав, вызовов внешних сервисов, отправки писем.
schemas — что приходит и что уходитRemoved
**Что здесь:** классы Pydantic — контракт API. Отдельно на вход, отдельно на выход. ```python class CategoryCreate(BaseSchema): # что клиент вправе прислать name: str parent_id: int | None = None class CategoryResponse(BaseSchema): # что мы отдаём id: int name: str slug: str ``` Обрати внимание: в `CategoryCreate` нет `id` и `slug` — их назначает сервер. **Чего здесь быть не должно:** запросов к базе и бизнес-правил.
repository — единственное место с SQLRemoved
**Что здесь:** всё, что ходит в базу. Запросы, выборки, вставки, удаления. ```python async def get_by_slug(self, slug: str) -> Category | None: stmt = select(Category).where(Category.slug == slug) return await self.session.scalar(stmt) ``` Репозиторий возвращает `None`, если не нашёл. Решать, ошибка это или нет, — не его дело. **Чего здесь быть не должно:** правил вроде «нельзя удалять категорию с товарами».
services — правила предметной областиRemoved
**Что здесь:** то, ради чего проект существует. На примере категорий: slug генерируется из названия; нельзя удалить категорию с потомками; имя уникально внутри родителя. ```python async def delete(self, category_id: int) -> None: if await self.repo.has_children(category_id): raise ConflictError("У категории есть подкатегории") await self.repo.delete(category_id) ``` Сервис бросает **своё** исключение, а не `HTTPException`. **Чего здесь быть не должно:** кодов ответа, объекта запроса, прямых запросов к базе.
routers — только HTTPRemoved
**Что здесь:** маршруты, коды ответа, модели ответа. Роутер тонкий: принял, вызвал сервис, отдал. ```python @router.post("", status_code=201, response_model=CategoryResponse) async def create_category( data: CategoryCreate, service: CategoryServiceDep, ) -> Category: return await service.create(data) ``` Три строки. Если в роутере появился `if` — скорее всего, это логика, и ей место в сервисе. **Чего здесь быть не должно:** запросов к базе, бизнес-правил, `try/except` вокруг вызовов.
core — то, что нужно всемRemoved
**Что здесь:** настройки, исключения, зависимости, безопасность, логирование. ``` core/ ├── settings/ конфиг из .env ├── exceptions/ свои исключения и обработчики ├── dependencies/ фабрики для Depends └── security/ пароли и токены ``` **Главная опасность:** `core` легко превращается в свалку для всего, чему не нашлось места. Проверка: если что-то из `core` нужно только одному слою — оно туда и должно переехать.
## Правило зависимостейRemoved
## Правило зависимостей Главное, что делает слои слоями, а не просто папками: > **Слой знает только про соседа снизу и никогда — про соседа сверху.** | Можно | Нельзя | |---|---| | роутер → сервис | сервис → роутер | | сервис → репозиторий | репозиторий → сервис | | репозиторий → модель | модель → что угодно выше | | любой слой → core | core → любой слой | Почему это важно: если зависимости идут в обе стороны, ты получаешь кольцевые импорты (Python упадёт с `ImportError`) или, что хуже, связный ком — где правка в одном месте ломает три других. Это не изобретение FastAPI. Мартин Фаулер описывал то же самое как **[Presentation Domain Data Layering](https://martinfowler.com/bliki/PresentationDomainDataLayering.html)** — разделение на представление, предметную область и данные.
Нарисовать путь запроса на бумагеRemoved
Без подглядывания в этот список нарисуй цепочку для запроса «создать категорию»: кто кого вызывает и что возвращает обратно. Потом сверь с блоком в начале списка.
Описать структуру в READMERemoved
Одна строка на каждую папку, своими словами. Не копируй из этого списка — смысл в том, чтобы сформулировать самому.
## Как это в боюRemoved
## Как это в бою В боевом проекте-ориентире — ровно эти же папки: `models`, `schemas`, `repository`, `services`, `routers`, `core`. Плюс седьмая — `infrastructure`, для внешних систем: хранилище файлов, очереди, внешние API. Разница только в наполнении: там 978 файлов `.py` против твоих шести пустых папок. Структура одна и та же — и именно поэтому в нём можно сориентироваться. Важное честное замечание: на таком масштабе у слоистой структуры появляется минус — чтобы добавить одну функцию, надо править файлы в пяти разных папках. Отсюда растут другие подходы — им посвящён отдельный список.
Removed
Removed
Removed
## ГотовоRemoved
## Готово Дерево создано, роли распределены, правило зависимостей понятно. Кода по-прежнему нет — и это нормально. **Перед тем как идти дальше,** пройди список «Какие вообще бывают структуры Python-проектов». Важно понимать, что мы выбрали один вариант из нескольких и у него есть цена, — иначе легко решить, что слои бывают всегда и везде. **Что дальше в проекте:** корневые файлы и `.env`, потом настройки, точка входа и первый эндпоинт.
Review