Структура проекта: пять слоёв внутри app и правило зависимостей
Разбираем каждую папку по отдельности: за что отвечает, что в неё класть нельзя и почему слои зависят друг от друга только в одну сторону. Создаём дерево до появления кода.
miki/struktura-proekta-pyat-sloev-vnutri-app-i-pravilo-zavisimost · v1
Разбираем каждую папку по отдельности: за что отвечает, что в неё класть нельзя и почему слои зависят друг от друга только в одну сторону. Создаём дерево до появления кода.
Когда файлов десять, структура кажется формальностью. Когда их девятьсот — она единственное, что позволяет найти нужное место за секунды.
Задать её сейчас стоит пятнадцать минут. Перекладывать потом сотни файлов, починяя импорты, — день работы и гарантированные ошибки.
Важно сразу: слоистая структура — не единственный вариант и не «правильный по определению». Есть модульная, гексагональная, плоская и другие — им посвящён отдельный список. Здесь мы берём слоистую, потому что на ней проще всего понять саму идею разделения ответственности.
Вся структура существует ради этой цепочки. Запомни её — дальше всё будет про неё:
И обратно тем же путём. Каждый слой говорит только со следующим. Роутер не знает про базу, модель не знает про HTTP.
Дерево
Внутри app/ создай шесть папок. Пока пустых — кода не будет ни строчки.
На Windows в PowerShell можно одной строкой, на macOS и в Git Bash — командой ниже.
mkdir -p app/{models,schemas,repository,services,routers,core}- Внутри app шесть папок
- Ни в одной пока нет кода
Файл пустой — ему не нужно содержимое, важен сам факт его существования.
Не забудь про сам app/__init__.py — uv его уже создал, проверь, что он на месте.
find app -type d -exec touch {}/__init__.py \;- В каждой папке внутри app есть __init__.py
- Файл app/__init__.py тоже на месте
- uv run python -c "import app" отрабатывает без ошибки
Дальше шаги не про команды, а про понимание. На каждом ответь себе на вопрос из проверок — если не получается, перечитай раздел «зачем», а не иди дальше.
Слои
Что здесь: классы SQLAlchemy — описание таблиц, колонок, связей, индексов и ограничений.
Чего здесь быть не должно: кодов ответа, проверок прав, вызовов внешних сервисов, отправки писем.
- Ты можешь объяснить, почему модель не должна знать про HTTP
- Понимаешь, чем модель отличается от схемы
Что здесь: классы Pydantic — контракт API. Отдельно на вход, отдельно на выход.
Обрати внимание: в CategoryCreate нет id и slug — их назначает сервер.
Чего здесь быть не должно: запросов к базе и бизнес-правил.
- Ты можешь объяснить, зачем разные классы на вход и на выход
- Понимаешь, почему id нет в схеме создания
Что здесь: всё, что ходит в базу. Запросы, выборки, вставки, удаления.
Репозиторий возвращает None, если не нашёл. Решать, ошибка это или нет, — не его дело.
Чего здесь быть не должно: правил вроде «нельзя удалять категорию с товарами».
- Ты можешь объяснить, почему репозиторий возвращает None, а не бросает ошибку
Что здесь: то, ради чего проект существует. На примере категорий: slug генерируется из названия; нельзя удалить категорию с потомками; имя уникально внутри родителя.
Сервис бросает своё исключение, а не HTTPException.
Чего здесь быть не должно: кодов ответа, объекта запроса, прямых запросов к базе.
- Ты можешь назвать три правила, которые относятся к сервису
- Понимаешь, почему сервис не бросает HTTPException
Что здесь: маршруты, коды ответа, модели ответа. Роутер тонкий: принял, вызвал сервис, отдал.
Три строки. Если в роутере появился if — скорее всего, это логика, и ей место в сервисе.
Чего здесь быть не должно: запросов к базе, бизнес-правил, try/except вокруг вызовов.
- Ты можешь объяснить, почему if в роутере — повод насторожиться
Что здесь: настройки, исключения, зависимости, безопасность, логирование.
Главная опасность: core легко превращается в свалку для всего, чему не нашлось места. Проверка: если что-то из core нужно только одному слою — оно туда и должно переехать.
- Ты можешь назвать критерий, по которому вещь попадает в core
Главное, что делает слои слоями, а не просто папками:
Слой знает только про соседа снизу и никогда — про соседа сверху.
| Можно | Нельзя |
|---|---|
| роутер → сервис | сервис → роутер |
| сервис → репозиторий | репозиторий → сервис |
| репозиторий → модель | модель → что угодно выше |
| любой слой → core | core → любой слой |
Почему это важно: если зависимости идут в обе стороны, ты получаешь кольцевые импорты (Python упадёт с ImportError) или, что хуже, связный ком — где правка в одном месте ломает три других.
Это не изобретение FastAPI. Мартин Фаулер описывал то же самое как Presentation Domain Data Layering — разделение на представление, предметную область и данные.
Проверка
Без подглядывания в этот список нарисуй цепочку для запроса «создать категорию»: кто кого вызывает и что возвращает обратно.
Потом сверь с блоком в начале списка.
- Цепочка нарисована по памяти
- Стрелки идут только в одну сторону
- Совпало с образцом
Одна строка на каждую папку, своими словами. Не копируй из этого списка — смысл в том, чтобы сформулировать самому.
- В README есть раздел «Структура проекта»
- Каждая папка описана одной строкой
- Формулировки твои, а не скопированные
В боевом проекте-ориентире — ровно эти же папки: models, schemas, repository, services, routers, core. Плюс седьмая — infrastructure, для внешних систем: хранилище файлов, очереди, внешние API.
Разница только в наполнении: там 978 файлов .py против твоих шести пустых папок. Структура одна и та же — и именно поэтому в нём можно сориентироваться.
Важное честное замечание: на таком масштабе у слоистой структуры появляется минус — чтобы добавить одну функцию, надо править файлы в пяти разных папках. Отсюда растут другие подходы — им посвящён отдельный список.
Дерево создано, роли распределены, правило зависимостей понятно. Кода по-прежнему нет — и это нормально.
Перед тем как идти дальше, пройди список «Какие вообще бывают структуры Python-проектов». Важно понимать, что мы выбрали один вариант из нескольких и у него есть цена, — иначе легко решить, что слои бывают всегда и везде.
Что дальше в проекте: корневые файлы и .env, потом настройки, точка входа и первый эндпоинт.

