Введение

Когда я встал на путь становления fullstack'ом, передо мной появилась проблема: Для того, чтобы спроектировать простенький api для тестирования своих frontend-приключений, необходимо создать fastapi-приложение, развернуть базу данных, настроить подключение к бд с помощью алхимии и так далее.

После нескольких почти одинаковых разворачиваний "простенькой" апишки, которые забирали достаточно много времени, я решил, что пора уже наконец создать репозиторий-шаблон для быстрого разворачивания FastAPI с минимально-необходимой инфраструктурой, чтобы он работал из коробки.

В этой статье я хочу привести свое видение того, как должна выглядеть базовая инфраструктура FastAPI, и описать, как, что и почему я решил включить в структуру своего fastapi-шаблона. Если вы знаете варианты, как сделать лучше - пишите в комментариях, я обязательно это попробую. Я планирую улучшать свой шаблон по мере того, как буду набираться знаний и опыта.

Ссылка не репозиторий с шаблоном: https://github.com/max31ru12/FastAPI-Template

Немного про мой опыт

Я работаю разработчиком в крупной IT-компании, занимающейся, в основном, работой с данными или же базами данных. На работе я использую Django и React, но я также имею опыт разработки на FastAPI в стартапе.

База данных

Я еще не оказывался в такой ситуации, чтобы с каким-то python-приложением мне было необходимо развернуть нечно иное, чем postgres. Поэтому в качестве субд была выбрана постгря, развернутая в докере.

Конфигурация файла docker-compose для БД:

services:
  postgres:
    container_name: postgres
    image: postgres:alpine
    environment:
      - POSTGRES_USER=test
      - POSTGRES_DB=test
      - POSTGRES_PASSWORD=test
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 30s
      timeout: 10s
      retries: 5
    networks:
      - default

Тут все довольно просто:

  • использую postgres:alpine для того, чтобы контейнер занимал меньше места;

  • прокидываю базовые переменные окружения для postgres'а;

  • волюм обязательно;

  • healthcheck по желанию.

В принципе, БД можно использовать любую, но самый частный кейс в моей практике - postgres. Если понадобится, например, Mongo, то придется написать новый сервис.

Структура бэкенда

Шаблон не должен навязывать слепое следование какой-то определенной структуре папок. Поэтому основная директория, где расположен бэкенд, не имеет строгой структуры файлов и папок. Основное приложение находится в директории app/, где расположены папки:

  • models - модели SQLAlchemy;

  • api - все, что связано у роутингом;

  • utils - какие-то вспомогательные функции;

  • entities - pydantic модели.

Все эти папки (за исключение, наверное, api/) опциональны. Можно их выносить, удалять, объединять и т.п. Важно отметить, что в больших монолитах внутри основной директории /app могут быть различные приложения, внутри которых и будут находиться вышеперечисленные папки.

Внутри main.py все стандартно. Из интересного могу отметить использование ORJSONResponse в создании инстанса приложения. Это позволяет ускорить работу с JSON, так как orjson - самая быстрая либа для работы с JSON (по крайней мере они так заявляют в своем репозитории).

В config.py я решил вынести все, что связано с каким-то константами и переменными окружения. setup_db.py содержит в себе все, что необходимо для конфигурации SQLAlchemy.

Миграции Alembic

Папку с миграциями alembic/ я решил расположить в корневой папке репозитория. Папка с миграциями у меня одна, поэтому ее нет особого смысла помещать в директорию с бэкендом. При появлении приложений внутри /app можно для каждого их них сделать отдельную папку с миграциями, как это реализовано в Django.

Инициализация таблиц - это полностью ответственность alembic, поэтому никакого ручного создания таблиц через:

async with test_engine.begin() as conn:
    await conn.run_sync(Base.metadata.create_all)

Тестирование

Все базовые настройки для тестирования эндпоинтов и взаимодействия с БД находятся в tests/contest.py, так также фикстура для сессий. Тестовая БД создается для всех прогонов тестов и удаляется в конце. В test_example.py два простых теста для того, чтобы пайплайн не прошел, если что-то связанное с БД сломалось.

CI/CD

Я написал простенький и не очень красивый пайплайн, который проверяет установку все зависимостей, прогоняет линтеры и тесты, а также поднимает docker-compose с БД и бэкендом.

Также есть pre-commit с несколькими линтерами.

Образ FastAPI

Для бэкенда я написал докер-файл, который находится по пути compose/backend/Dockerfile. В принципе, можно было оставить этот файл в корневой папке проекта, но что если надо будет написать еще один Dockerfile для какого-нибудь celery-воркера? Именно для таких ситуаций я разделил докер-файлы по папкам внутри директории compose.

Конфигурация докер-файла:

FROM python:3.12-slim

RUN apt-get update -qy

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

WORKDIR /backend

COPY requirements-dev.txt ./

RUN pip install -r requirements-dev.txt

COPY ./app ./app
COPY ./alembic ./
COPY alembic.ini ./

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--reload"]

EXPOSE 8000

В образе все стандартно:

  • Образ python использую slim ради экономии места;

  • Использую pip. Иногда я использую poetry для fastapi-проектов, но пока что я не увидел на практике явных преимуществ poetry по сравнению с pip. Понятное дело, что они есть, но они не всегда нужны;

  • alembic - папка с миграциями. Можно перенести ее в директорию приложения app;

  • alembic.ini меняться вряд ли будет, поэтому скопируем его отдельной командой, чтобы docker его закэшировал.

Сервис для бэкенда в docker-compose файле:

  backend:
    container_name: backend
    build:
      context: .
      dockerfile: compose/backend/Dockerfile
    depends_on:
      - postgres
    environment:
      - DB_HOST=${DB_HOST:-postgres}
      - DB_PORT=${DB_PORT:-5432}
      - DB_PASSWORD=${DB_PASSWORD:-test}
      - DB_USER=${DB_USER:-test}
      - DB_NAME=${DB_NAME:-test}
      - DEV_MODE=${DEV_MODE:-true}
    ports:
      - "8000:8000"
    restart: unless-stopped
    volumes:
      - ./app/:/backend/app/
      - ./alembic/:/backend/alembic
    networks:
      - default

Запуск приложения

Любое приложение, построенное на основе моего шаблона, запускается с помощью команды:

docker compose -f local.yml up --build -d

После запуска контейнеров бэкенд будет доступен по стандартному адресу.

Миграции проводятся через контейнер с бэкендом:

docker compose -f ./local.yml run --rm backend alembic upgrade head

Аналогично можно создавать миграции:

docker compose -f ./local.yml run --rm backend alembic revision --autogenerate -m "name" --rev-id "001"

Заключение

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

Из явных проблем могу отметить небольшие танцы с бубном при использовании удаленного интерпретатора из докера, но это терпимо. Также можно написать более красивый пайплайн и, при необходимости, более продуманно разместить миграции и конфигурационные файлы, но это вкусовщина и зависит от масштаба бэкенда.

Комментарии (16)


  1. Mephistofx
    31.12.2024 10:08

    Подскажите, а для чего в Dockerfile есть

    COPY .app/ .app/

    Если в docker-compose директория прокидывается через volume и работать будет и без COPY


    1. max31ru12 Автор
      31.12.2024 10:08

      Да, вы абсолютно правы, эту строчку можно вообще убрать из докер-файла


      1. yesworldd
        31.12.2024 10:08

        Стоп, почему убирать? Docker compose же нужен для локальной разработки


        1. max31ru12 Автор
          31.12.2024 10:08

          Потому волюм в docker-compose и так прокидывает эту директорию в контейнер. Docker compose не всегда нужен только для локальной разработки. Например, с его помощью можно накатывать ПО для заказчиков и не возиться с особенностями установленной у него операционки


  1. Ver_P
    31.12.2024 10:08

    А не проще использовать готовые шаблоны, например Cookiecutter? Для Джанго приложений полностью на него перешёл - удобно и быстро, только надо разобраться в структуре проекта)


    1. max31ru12 Автор
      31.12.2024 10:08

      В зависимости от того, что вам нужно. Шаблон Cookiecutter для fastapi содержит в себе очень много всего, не всегда столько нужно для простого api. Да и кажется его больше не поддерживают (последнее обновление 4 года назад)


      1. VPryadchenko
        31.12.2024 10:08

        Есть вот вроде от самого fastapi: https://github.com/fastapi/full-stack-fastapi-template


  1. Pubert
    31.12.2024 10:08

    На всякий случай - uvicorn с опцией --reload потребляет очень много ресурсов и может тормозить, о чём говорится прям в документации) поэтому использовать на проде крайне не рекомендуется (можно в том случае, если ресурсов предостаточно, или высокая производительность не требуется)


  1. roslovets
    31.12.2024 10:08

    А SQLModel пробовали? Он объединяет Sqlalchemy с Pydantic. В моих проектах на fastapi хорошо заходит, но бывают некоторые неудобства из-за абстракции поверх алхимии.


    1. max31ru12 Автор
      31.12.2024 10:08

      Пока не пробовал. Наверное, стоит попробовать. Есть либа, которая позволяет маппить модели sqlalchemy в модели pydantic. Называется pydantic-marshals, тоже довольно удобная вещь


  1. roslovets
    31.12.2024 10:08

    И ещё вопрос, почему requirements.txt, а не pyproject.toml, где можно гибко управлять зависимостями?


    1. max31ru12 Автор
      31.12.2024 10:08

      Потому что это самый простой вариант. Его всегда можно легко и просто заменить на pyproject.toml, при желании


  1. iantoshkai
    31.12.2024 10:08

    вместо pip советую uv


    1. max31ru12 Автор
      31.12.2024 10:08

      Спасибо, посмотрю!


  1. gyolkin
    31.12.2024 10:08

    Мне кажется, в тексте не хватает двух слов о существующих шаблонах "FastAPI приложений", которых на GitHub действительно много. Например, full-stack-fastapi-template от tiangolo, создателя этого фреймворка. Мотивация автора понятна, но совсем не понятно, почему не решили использовать один из существующих шаблонов? Что в этом шаблоне есть такого, чего нет в уже существующих?

    Также хочу отметить, что вы лишаете свое приложение гибкости и масштабируемости, используя подобную структуру. FastAPI - это микрофреймворк. Легкий инструмент, чтобы принимать запросы, передавать полученную информацию куда-то еще, возвращать ответ пользователю. Веб-фреймворк принадлежит к "внешнему кругу", если выражаться в терминах чистой архитектуры, и это неспроста. Роль FastAPI в приложении весьма примитивна и ограниченна, однако в такой структуре он стоит в центре, что подтверждает и автор статьи:

    все эти папки (за исключение, наверное, api/) опциональны...

    В случае автора статьи эта проблема может быть и не проблемой, поскольку мотивация в разворачивании "простеньких" апишек для тестирования фронтенда. Но для некоторых читателей однажды это может стать причиной серьезного рефакторинга, когда с разрастанием кодовой базы станут очевидны недостатки, превращающиеся в боли.


    1. max31ru12 Автор
      31.12.2024 10:08

      Спасибо за конструктивный комментарий, постараюсь так же конструктивно ответить.

      Я не упоминал о существующих шаблонах, по нескольким причинам:
      1) Я с ними слабо знаком и не использую их (хотя я понимаю, что стоит ознакомиться)
      2) Они часто имеют избыточный функционал (как пример, cookiecutter содержит в себе фронт на Vue, а зачем он мне нужен, если я пишу простые CRUD'ы просто чтобы попрактиковаться)

      Насчет гибкости и масштабируемости хотел бы узнать более подробно. Почему моя структура плоха? Не могли бы еще привести пример проблем моего подхода с разрастанием кодовой базы? Это позволило бы мне пересмотреть свой подход и улучшить его.