Введение
Когда я встал на путь становления 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)
 - Ver_P31.12.2024 10:08- А не проще использовать готовые шаблоны, например Cookiecutter? Для Джанго приложений полностью на него перешёл - удобно и быстро, только надо разобраться в структуре проекта)  - max31ru12 Автор31.12.2024 10:08- В зависимости от того, что вам нужно. Шаблон Cookiecutter для fastapi содержит в себе очень много всего, не всегда столько нужно для простого api. Да и кажется его больше не поддерживают (последнее обновление 4 года назад)  - VPryadchenko31.12.2024 10:08- Есть вот вроде от самого fastapi: https://github.com/fastapi/full-stack-fastapi-template 
 
 
 - Pubert31.12.2024 10:08- На всякий случай - uvicorn с опцией --reload потребляет очень много ресурсов и может тормозить, о чём говорится прям в документации) поэтому использовать на проде крайне не рекомендуется (можно в том случае, если ресурсов предостаточно, или высокая производительность не требуется) 
 - roslovets31.12.2024 10:08- А SQLModel пробовали? Он объединяет Sqlalchemy с Pydantic. В моих проектах на fastapi хорошо заходит, но бывают некоторые неудобства из-за абстракции поверх алхимии.  - max31ru12 Автор31.12.2024 10:08- Пока не пробовал. Наверное, стоит попробовать. Есть либа, которая позволяет маппить модели sqlalchemy в модели pydantic. Называется pydantic-marshals, тоже довольно удобная вещь 
 
 - gyolkin31.12.2024 10:08- Мне кажется, в тексте не хватает двух слов о существующих шаблонах "FastAPI приложений", которых на GitHub действительно много. Например, full-stack-fastapi-template от tiangolo, создателя этого фреймворка. Мотивация автора понятна, но совсем не понятно, почему не решили использовать один из существующих шаблонов? Что в этом шаблоне есть такого, чего нет в уже существующих? - Также хочу отметить, что вы лишаете свое приложение гибкости и масштабируемости, используя подобную структуру. FastAPI - это микрофреймворк. Легкий инструмент, чтобы принимать запросы, передавать полученную информацию куда-то еще, возвращать ответ пользователю. Веб-фреймворк принадлежит к "внешнему кругу", если выражаться в терминах чистой архитектуры, и это неспроста. Роль FastAPI в приложении весьма примитивна и ограниченна, однако в такой структуре он стоит в центре, что подтверждает и автор статьи: - все эти папки (за исключение, наверное, api/) опциональны... - В случае автора статьи эта проблема может быть и не проблемой, поскольку мотивация в разворачивании "простеньких" апишек для тестирования фронтенда. Но для некоторых читателей однажды это может стать причиной серьезного рефакторинга, когда с разрастанием кодовой базы станут очевидны недостатки, превращающиеся в боли.  - max31ru12 Автор31.12.2024 10:08- Спасибо за конструктивный комментарий, постараюсь так же конструктивно ответить. 
 Я не упоминал о существующих шаблонах, по нескольким причинам:
 1) Я с ними слабо знаком и не использую их (хотя я понимаю, что стоит ознакомиться)
 2) Они часто имеют избыточный функционал (как пример, cookiecutter содержит в себе фронт на Vue, а зачем он мне нужен, если я пишу простые CRUD'ы просто чтобы попрактиковаться)- Насчет гибкости и масштабируемости хотел бы узнать более подробно. Почему моя структура плоха? Не могли бы еще привести пример проблем моего подхода с разрастанием кодовой базы? Это позволило бы мне пересмотреть свой подход и улучшить его. 
 
 
           
 
Mephistofx
Подскажите, а для чего в Dockerfile есть
COPY .app/ .app/
Если в docker-compose директория прокидывается через volume и работать будет и без COPY
max31ru12 Автор
Да, вы абсолютно правы, эту строчку можно вообще убрать из докер-файла
yesworldd
Стоп, почему убирать? Docker compose же нужен для локальной разработки
max31ru12 Автор
Потому волюм в docker-compose и так прокидывает эту директорию в контейнер. Docker compose не всегда нужен только для локальной разработки. Например, с его помощью можно накатывать ПО для заказчиков и не возиться с особенностями установленной у него операционки