$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогFastAPI для новичков: как организовать структуру проекта, чтобы не писать всё в одном файле

FastAPI для новичков: как организовать структуру проекта, чтобы не писать всё в одном файле

$ sudo teach IT
·26 июля 2026 г.·9 мин·93
FastAPI для новичков: как организовать структуру проекта, чтобы не писать всё в одном файле

Разложим приложение на роуты, сервисы и слой моделей, настроим зависимости и обработчики ошибок. Цель — база для дальнейшего роста без будущего “рефакторинга на боль”.

Содержание
Почему “всё в одном файле” — это не только неудобноЦелевая структура проекта: роуты, сервисы, моделиПример структуры директорийСлой моделей: Pydantic схемы без смешения логикиapp/models/users.pyСлой сервисов: бизнес-логика отдельно от HTTPapp/services/users.pyРепозиторий и доступ к данным: базовая заготовкаapp/db/repositories/users.pyЗависимости FastAPI: DI вместо ручных конструкторовapp/api/routes/users.py + зависимостьЕдинообразная обработка ошибок: чтобы сервис не “знал HTTP”app/errors/handlers.pymain.py: склейка приложения без “мегафайла”app/main.pyКак “правильно” масштабировать структуру дальше1) Не дублируйте зависимости в каждом роуте2) Не смешивайте “domain-ошибки” и “формат ответов”3) Разделяйте Pydantic схемы входа/выхода4) Следите за “толщиной” сервисовПаттерн “тонкие роуты” на практике: что именно считать тонкимТестирование: почему эта структура облегчает жизньЧастые подводные камни начинающихОшибка 1: использовать “модели” как ORM и как схемы одновременноОшибка 2: ловить исключения в каждом роутеОшибка 3: хранить зависимости “глобально”Ошибка 4: не контролировать формат ошибок валидацииНебольшой пример того, как это выглядит в ответах API

Новички часто начинают с FastAPI так: открывают main.py, добавляют несколько роутов, пару схем Pydantic, формируют запросы в обработчиках — и через неделю получают файл на 800–1200 строк, где бизнес-логика вперемешку с HTTP-деталями, валидацией и доступом к данным. Рефакторинг превращается в боль: меняешь одну часть — ломается другая, а тестировать становится сложно.

Хорошая новость: правильно организованная структура почти всегда окупается. Она не только делает код читаемым, но и снижает риски при росте проекта — особенно когда приложение выходит за рамки “простого API”.

Ниже разберём базовую, но масштабируемую структуру проекта на FastAPI: разложим приложение на роуты, сервисы и слой моделей/схем, настроим зависимости и обработчики ошибок. Это фундамент, с которого разумно начинать, чтобы не “прибивать” будущее рефакторингом.


Почему “всё в одном файле” — это не только неудобно

FastAPI поощряет быстрое прототипирование. Это отлично для старта, но у “одного файла” есть объективные минусы:

  1. Смешение уровней абстракции.
    HTTP-слой (маршруты, коды статусов, формат ответа) смешивается с бизнес-правилами (что разрешено, что запрещено), а те — с доступом к данным (как именно запрашивать данные).

  2. Непредсказуемые зависимости.
    Когда всё в одном файле, логика начинает “знать” слишком много: импорты циклятся, сложнее выделить повторяемые части, проще случайно создать скрытые зависимости.

  3. Усложнение тестирования.
    Если вы проверяете логику только через HTTP (интеграционные тесты), то тесты становятся медленными и хрупкими. Если бизнес-логика находится в обработчиках, тестировать её изолированно почти невозможно.

  4. Сложность внесения изменений.
    Допустим, вы меняете структуру данных или способ получения объектов. Если эти изменения “размазаны” по роутам и моделям, вы будете править много мест.

Правильная архитектура не делает приложение “идеальным”. Она делает его управляемым.


Целевая структура проекта: роуты, сервисы, модели

Рассмотрим пример простого API: управление пользователями. У нас будут:

  • Router слой: принимает входные данные, вызывает сервисы, возвращает ответ.
  • Service слой: содержит бизнес-логику (например, “создать пользователя”, “получить пользователя”).
  • Models / Schemas слой: Pydantic-схемы для входа/выхода и (при необходимости) доменные модели.
  • Dependencies: создание и внедрение зависимостей (например, репозиторий или “клиент БД”).
  • Error handling: единообразная обработка ошибок.

Пример структуры директорий

code
app/
  __init__.py
  main.py
  api/
    __init__.py
    routes/
      __init__.py
      users.py
  core/
    __init__.py
    config.py
  models/
    __init__.py
    users.py
  services/
    __init__.py
    users.py
  db/
    __init__.py
    session.py
    repositories/
      __init__.py
      users.py
  errors/
    __init__.py
    handlers.py

Вы можете адаптировать названия под себя. Главное — держать границы и назначение компонентов.


Слой моделей: Pydantic схемы без смешения логики

Сначала выделим схемы данных. В FastAPI чаще всего под “моделями” имеют в виду Pydantic-схемы: они описывают вход и выход HTTP.

app/models/users.py

code
from pydantic import BaseModel, EmailStr
from typing import Optional
from uuid import UUID, uuid4


class UserBase(BaseModel):
    email: EmailStr
    full_name: Optional[str] = None


class UserCreate(UserBase):
    password: str


class UserUpdate(BaseModel):
    full_name: Optional[str] = None
    email: Optional[EmailStr] = None


class UserOut(UserBase):
    id: UUID

    class Config:
        from_attributes = True  # полезно при ORM-моделях


class UserLoginRequest(BaseModel):
    email: EmailStr
    password: str

Пояснение по сути:

  • UserCreate — то, что клиент отправляет при создании.
  • UserOut — то, что API возвращает клиенту.
  • UserUpdate — частичное обновление (по желанию можно сделать PATCH-логику).

Важно: в схемах нет вызовов БД, нет бизнес-логики и тем более нет деталей HTTP (статусы, Request/Response).


Слой сервисов: бизнес-логика отдельно от HTTP

Теперь создадим сервис users, который будет работать с репозиторием (здесь репозиторий условный, ниже будет пример).

app/services/users.py

code
from uuid import UUID

from app.db.repositories.users import UsersRepository
from app.models.users import UserCreate, UserOut, UserUpdate


class UserService:
    def __init__(self, repo: UsersRepository):
        self.repo = repo

    async def create_user(self, payload: UserCreate) -> UserOut:
        # пример бизнес-правила: email должен быть уникальным
        existing = await self.repo.get_by_email(payload.email)
        if existing is not None:
            # не превращаем HTTP-ответ в сервисе
            raise ValueError("User with this email already exists")

        created = await self.repo.create(
            email=payload.email,
            full_name=payload.full_name,
            password=payload.password,
        )
        return UserOut(id=created["id"], email=created["email"], full_name=created["full_name"])

    async def get_user(self, user_id: UUID) -> UserOut:
        user = await self.repo.get_by_id(user_id)
        if user is None:
            raise LookupError("User not found")
        return UserOut(id=user["id"], email=user["email"], full_name=user["full_name"])

    async def update_user(self, user_id: UUID, payload: UserUpdate) -> UserOut:
        user = await self.repo.get_by_id(user_id)
        if user is None:
            raise LookupError("User not found")

        updated = await self.repo.update(
            user_id=user_id,
            email=payload.email if payload.email is not None else user["email"],
            full_name=payload.full_name if payload.full_name is not None else user["full_name"],
        )
        return UserOut(id=updated["id"], email=updated["email"], full_name=updated["full_name"])

Обратите внимание на ключевую идею: сервис выбрасывает исключения, а не формирует HTTP-ответы.

Да, на уровне “чистой архитектуры” можно использовать свои domain-исключения (UserAlreadyExists, UserNotFound). Но даже с базовыми исключениями важно держать границу: HTTP-слой решает, какой статус вернуть, сервис — что произошло по смыслу.


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

Поскольку вопрос статьи — структура и зависимости, сделаем репозиторий “заглушкой”, но правильно оформим границы.

app/db/repositories/users.py

code
from typing import Optional
from uuid import UUID, uuid4


class UsersRepository:
    """Пример интерфейса репозитория.
    В реальном проекте здесь будет доступ к БД (SQLAlchemy, asyncpg, etc).
    """

    def __init__(self):
        # демо-хранилище
        self._store = {}

    async def get_by_id(self, user_id: UUID) -> Optional[dict]:
        return self._store.get(str(user_id))

    async def get_by_email(self, email: str) -> Optional[dict]:
        for u in self._store.values():
            if u["email"] == email:
                return u
        return None

    async def create(self, email: str, full_name: str, password: str) -> dict:
        user_id = uuid4()
        user = {"id": user_id, "email": email, "full_name": full_name, "password": password}
        self._store[str(user_id)] = user
        return user

    async def update(self, user_id: UUID, email: str, full_name: str) -> dict:
        user = self._store[str(user_id)]
        user["email"] = email
        user["full_name"] = full_name
        self._store[str(user_id)] = user
        return user

В реальном проекте вам понадобится:

  • сессия БД,
  • транзакции,
  • конфигурация подключения,
  • вероятно, отдельный слой моделей ORM.

Но базовый каркас остаётся: роуты не должны знать детали хранилища.


Зависимости FastAPI: DI вместо ручных конструкторов

FastAPI умеет внедрять зависимости через Depends. Это тот случай, когда DI реально делает код чище, а не “усложняет”.

Создадим функцию зависимостей: репозиторий и сервис.

app/api/routes/users.py + зависимость

code
from typing import List
from uuid import UUID

from fastapi import APIRouter, Depends, status

from app.db.repositories.users import UsersRepository
from app.models.users import UserCreate, UserOut, UserUpdate
from app.services.users import UserService

router = APIRouter(prefix="/users", tags=["users"])


def get_users_repo() -> UsersRepository:
    # В реальном приложении — создание сессии/репозитория по request scope
    return UsersRepository()


def get_user_service(repo: UsersRepository = Depends(get_users_repo)) -> UserService:
    return UserService(repo=repo)


@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
async def create_user(
    payload: UserCreate,
    service: UserService = Depends(get_user_service),
):
    return await service.create_user(payload)


@router.get("/{user_id}", response_model=UserOut)
async def get_user(
    user_id: UUID,
    service: UserService = Depends(get_user_service),
):
    return await service.get_user(user_id)


@router.patch("/{user_id}", response_model=UserOut)
async def update_user(
    user_id: UUID,
    payload: UserUpdate,
    service: UserService = Depends(get_user_service),
):
    return await service.update_user(user_id, payload)

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

  • UsersRepository создаётся в зависимости, а не в каждом роуте.
  • UserService также создаётся в зависимости, и роуты становятся тонкими.
  • Роуты читаются как “поток”: входные данные → сервис → результат.

Типичная ошибка: пытаться внедрять сервис вручную (service = UserService(...)) прямо в обработчиках. Да, это работает, но вы проигрываете гибкость (scope, тестирование, подмена зависимостей).


Единообразная обработка ошибок: чтобы сервис не “знал HTTP”

Теперь подключим обработчики ошибок. Идея:

  • сервис бросает исключения,
  • middleware/exception handler переводит их в понятные HTTP-ответы.

app/errors/handlers.py

code
from fastapi import Request
from fastapi.responses import JSONResponse


def http_error_response(status_code: int, message: str):
    return JSONResponse(
        status_code=status_code,
        content={"detail": message},
    )

Немного не хватает обработчиков. Давайте добавим конкретику:

code
from fastapi import Request, status
from fastapi.responses import JSONResponse


async def value_error_handler(request: Request, exc: ValueError):
    return JSONResponse(
        status_code=status.HTTP_409_CONFLICT,
        content={"detail": str(exc)},
    )


async def lookup_error_handler(request: Request, exc: LookupError):
    return JSONResponse(
        status_code=status.HTTP_404_NOT_FOUND,
        content={"detail": str(exc)},
    )

Теперь подключим это в main.py.


main.py: склейка приложения без “мегафайла”

Файл main.py должен быть коротким и отвечать за:

  • создание приложения,
  • подключение роутов,
  • регистрацию обработчиков ошибок,
  • (опционально) CORS, логирование, startup/shutdown.

app/main.py

code
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

from app.api.routes.users import router as users_router
from app.errors.handlers import value_error_handler, lookup_error_handler


app = FastAPI(title="Example API")


@app.exception_handler(ValueError)
async def value_error_exc_handler(request: Request, exc: ValueError):
    return await value_error_handler(request, exc)


@app.exception_handler(LookupError)
async def lookup_error_exc_handler(request: Request, exc: LookupError):
    return await lookup_error_handler(request, exc)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    # Приведём формат ошибок к более “человеческому”
    return JSONResponse(
        status_code=422,
        content={"detail": exc.errors()},
    )


app.include_router(users_router)

Обратите внимание:

  • обработчики ошибок вынесены и подключаются централизованно,
  • main.py не содержит бизнес-логики.

Как “правильно” масштабировать структуру дальше

Базовая архитектура хорошо выдерживает рост. Но есть нюансы, о которые часто спотыкаются на этапе “после первых роутов”.

1) Не дублируйте зависимости в каждом роуте

Если вы для каждого роутера пишете:

code
repo = UsersRepository()
service = UserService(repo)

— вы фактически игнорируете DI. Правильнее:

  • один раз описать get_repo() и get_service(),
  • переиспользовать через Depends.

2) Не смешивайте “domain-ошибки” и “формат ответов”

Если сервис начинает возвращать JSONResponse или HTTPException, он “привязан” к HTTP. Это ломает тестирование и переносимость логики.

Вместо этого:

  • сервис бросает доменные ошибки,
  • exception handler преобразует их в HTTP.

3) Разделяйте Pydantic схемы входа/выхода

Частая ошибка: использовать одну схему для всего.

Например, UserOut не должен включать пароль.
А UserCreate не должен возвращаться в ответ как “всё о пользователе”.

Разные схемы — это не бюрократия, а защита от утечек и случайных несовместимостей.

4) Следите за “толщиной” сервисов

Если сервис начинает собирать сложные составные DTO, ходить в несколько репозиториев и ещё форматировать ответ под HTTP — это сигнал, что нужно разнести:

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

На начальном этапе можно и проще, но держите это как ориентир.


Паттерн “тонкие роуты” на практике: что именно считать тонким

“Тонкий роут” — это не про количество строк. Это про то, что роут:

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

Роут в идеале выглядит как адаптер между HTTP и внутренней логикой.


Тестирование: почему эта структура облегчает жизнь

В этой архитектуре обычно появляются как минимум два типа тестов:

  1. Unit-тесты сервисов
    Подменяете UsersRepository заглушкой (in-memory или mock), проверяете бизнес-логику.

  2. Integration-тесты API
    Проверяете, что роут правильно подключён, что валидация и exception handlers дают корректный статус и формат ответа.

Если бы всё было в одном файле, вы бы:

  • тестировали много HTTP-уровня ради проверки простого правила,
  • чаще сталкивались бы с “сломанными” тестами при рефакторинге деталей.

Частые подводные камни начинающих

Ошибка 1: использовать “модели” как ORM и как схемы одновременно

Иногда новички делают класс модели, который и хранит данные БД, и используется как ответ. Потом меняют БД и ломают API-формат.

Решение:

  • ORM (если используете) отдельно,
  • Pydantic схемы отдельно.

Ошибка 2: ловить исключения в каждом роуте

Код вроде:

code
try:
    return await service.create_user(payload)
except ValueError as e:
    raise HTTPException(status_code=409, detail=str(e))

— приводит к дублированию и несогласованности формата ошибок.

Решение: единый exception handler.

Ошибка 3: хранить зависимости “глобально”

Например, создать репозиторий и сервис один раз на модульном уровне. Это плохо, если зависимости содержат контекст запроса (сессии, транзакции, пользовательские данные).

Решение: DI через Depends, и scope аккуратно.

Ошибка 4: не контролировать формат ошибок валидации

По умолчанию FastAPI возвращает стандартизированный формат 422. Многие его переопределяют, но делают это частично и получают странные структуры.

Решение: если переопределяете — делайте единообразно и тестируйте.


Небольшой пример того, как это выглядит в ответах API

После запуска вы получите, например:

  • POST /users — 201 с UserOut
  • GET /users/{id} — 200 или 404
  • при попытке создать пользователя с существующим email — 409

Роуты при этом выглядят предсказ

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

API на FastAPI

Курс по теме

API на FastAPI

Погрузитесь в мир самого быстрого фреймворка в Python – FastAPI! Курс для тех, кто хочет освоить полноценную профессию, и писать эндпоинты на enterprise-уровне!

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

Перейти к курсу

Другие статьи

Запуск и поддержка проекта на Ruff в команде: формат “как в CI”, а не “как договорились”
запуск

Запуск и поддержка проекта на Ruff в команде: формат “как в CI”, а не “как договорились”

Как правильно выстроить конфигурацию Ruff (линтинг, форматирование, автозамены) и сделать так, чтобы одинаковые правила гарантировались в локальной сборке и на CI без ручного шаманства.

16 сентября 2026 г.
1700
FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь
fastapi

FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь

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

22 июля 2026 г.
1160
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

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

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

21 сентября 2026 г.
1970
Как начать с HTML и сделать первую страницу: пошагово и без сложных слов
html

Как начать с HTML и сделать первую страницу: пошагово и без сложных слов

Разберём, что нужно новичку, чтобы за 1–2 вечера собрать свою первую веб-страницу: структура HTML, базовые теги, ссылки и картинки, плюс простой чек-лист, как понять, что страница работает правильно.

2 ноября 2026 г.
20
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1960
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
130

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему “всё в одном файле” — это не только неудобноЦелевая структура проекта: роуты, сервисы, моделиПример структуры директорийСлой моделей: Pydantic схемы без смешения логикиapp/models/users.pyСлой сервисов: бизнес-логика отдельно от HTTPapp/services/users.pyРепозиторий и доступ к данным: базовая заготовкаapp/db/repositories/users.pyЗависимости FastAPI: DI вместо ручных конструкторовapp/api/routes/users.py + зависимостьЕдинообразная обработка ошибок: чтобы сервис не “знал HTTP”app/errors/handlers.pymain.py: склейка приложения без “мегафайла”app/main.pyКак “правильно” масштабировать структуру дальше1) Не дублируйте зависимости в каждом роуте2) Не смешивайте “domain-ошибки” и “формат ответов”3) Разделяйте Pydantic схемы входа/выхода4) Следите за “толщиной” сервисовПаттерн “тонкие роуты” на практике: что именно считать тонкимТестирование: почему эта структура облегчает жизньЧастые подводные камни начинающихОшибка 1: использовать “модели” как ORM и как схемы одновременноОшибка 2: ловить исключения в каждом роутеОшибка 3: хранить зависимости “глобально”Ошибка 4: не контролировать формат ошибок валидацииНебольшой пример того, как это выглядит в ответах API