Введение
Когда я встал на путь становления 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_P
31.12.2024 10:08А не проще использовать готовые шаблоны, например Cookiecutter? Для Джанго приложений полностью на него перешёл - удобно и быстро, только надо разобраться в структуре проекта)
max31ru12 Автор
31.12.2024 10:08В зависимости от того, что вам нужно. Шаблон Cookiecutter для fastapi содержит в себе очень много всего, не всегда столько нужно для простого api. Да и кажется его больше не поддерживают (последнее обновление 4 года назад)
VPryadchenko
31.12.2024 10:08Есть вот вроде от самого fastapi: https://github.com/fastapi/full-stack-fastapi-template
Pubert
31.12.2024 10:08На всякий случай - uvicorn с опцией --reload потребляет очень много ресурсов и может тормозить, о чём говорится прям в документации) поэтому использовать на проде крайне не рекомендуется (можно в том случае, если ресурсов предостаточно, или высокая производительность не требуется)
roslovets
31.12.2024 10:08А SQLModel пробовали? Он объединяет Sqlalchemy с Pydantic. В моих проектах на fastapi хорошо заходит, но бывают некоторые неудобства из-за абстракции поверх алхимии.
max31ru12 Автор
31.12.2024 10:08Пока не пробовал. Наверное, стоит попробовать. Есть либа, которая позволяет маппить модели sqlalchemy в модели pydantic. Называется pydantic-marshals, тоже довольно удобная вещь
gyolkin
31.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 не всегда нужен только для локальной разработки. Например, с его помощью можно накатывать ПО для заказчиков и не возиться с особенностями установленной у него операционки