Практический Flask: реальный проект от идеи до продакшена

Курс посвящён разработке полноценного веб-приложения на Flask на примере реального проекта. Вы пройдёте путь от проектирования и настройки окружения до разработки API, работы с базой данных, тестирования, безопасности и деплоя в продакшен.

1. Архитектура проекта и настройка окружения

Архитектура проекта и настройка окружения

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

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

Какой проект мы будем строить

В качестве учебного проекта возьмём небольшой веб-сервис с типичным набором требований:

  • веб-интерфейс
  • JSON API
  • база данных
  • авторизация
  • фоновые задачи и интеграции (в следующих статьях)
  • Важно не то, какой именно сервис, а то, что его архитектура должна выдерживать рост: новые страницы, новые API-ручки, новые модели и интеграции.

    Принципы архитектуры для Flask-проекта

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

    Ниже принципы, на которых мы будем строить проект.

    Явные границы слоёв

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

  • HTTP-слой: маршруты, валидация входных данных, формирование ответа
  • бизнес-логика: правила предметной области (например, что можно/нельзя делать пользователю)
  • слой данных: работа с базой и внешними сервисами
  • Так проще тестировать и менять реализацию (например, заменить способ хранения данных) без переписывания всего приложения.

    Фабрика приложения

    Фабрика приложения — это функция, которая создаёт и настраивает экземпляр Flask-приложения. Это стандартный паттерн в Flask, полезный для:

  • разных конфигураций (dev/test/prod)
  • тестирования (создаём приложение заново для каждого набора тестов)
  • расширений Flask (инициализация в одном месте)
  • Официальный паттерн описан в документации Flask: Application Factories.

    Модули через blueprints

    Blueprint — механизм, который позволяет группировать маршруты по смыслу (например, auth, api, web). Это помогает держать структуру проекта чистой и масштабируемой.

    Документация: Blueprints.

    Конфигурация через переменные окружения

    Мы будем избегать “зашитых” настроек в коде:

  • ключи, пароли, DSN базы данных — только через переменные окружения
  • разные окружения (локально/тесты/прод) — через разные наборы переменных
  • Документация Flask по настройкам: Configuration Handling.

    !Диаграмма показывает разделение ответственности и направление зависимостей

    Структура проекта

    Ниже рекомендуемая структура, к которой мы будем придерживаться в курсе.

    !Иллюстрация структуры проекта и расположения ключевых модулей

    Рекомендуемое дерево каталогов

    Что где лежит

    | Часть | Зачем нужна | Типичные примеры | |---|---|---| | app/__init__.py | фабрика приложения, регистрация blueprint, подключение конфигурации | create_app() | | app/config.py | классы конфигурации | DevelopmentConfig, TestingConfig | | app/extensions.py | подключение расширений Flask в одном месте | SQLAlchemy, Migrate, LoginManager | | app/blueprints/* | маршруты, разбитые по смыслу | auth/routes.py, api/routes.py | | app/services/* | бизнес-логика (правила) | регистрация пользователя, выдача токена | | app/repositories/* | работа с базой и запросами | UserRepository.get_by_email() | | app/models/* | модели данных (ORM) | User, Session | | tests/* | автоматические тесты | pytest |

    Настройка окружения разработки

    Цель: получить воспроизводимую среду, которая одинаково работает у всех (и на CI в будущем).

    Версия Python

    Для курса ориентируйтесь на актуальный Python 3.11+ (если у вас 3.10 — тоже подойдёт, но лучше обновиться).

    Виртуальное окружение

    Виртуальное окружение — это отдельная папка, куда устанавливаются зависимости проекта, чтобы они не смешивались с глобальными пакетами системы.

    Официальная документация: venv — Creation of virtual environments.

    Создание и активация:

    Проверка:

    Установка зависимостей

    Мы разделим зависимости на:

  • основные (для работы приложения)
  • dev-зависимости (для линтинга, форматирования, тестов)
  • requirements.txt (пример минимального набора):

    requirements-dev.txt:

    Установка:

    Если pip для вас новый инструмент, ориентируйтесь на документацию: pip documentation.

    Точка входа: фабрика приложения

    Создадим минимальный каркас, чтобы приложение уже запускалось, но при этом соответствовало архитектуре.

    app/__init__.py

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

  • create_app(...) создаёт новый экземпляр Flask-приложения
  • config_object — строка с путём до класса конфигурации
  • /health — простой эндпоинт для проверки, что сервис жив (позже будет полезно для мониторинга и контейнеров)
  • Конфигурация проекта

    Сделаем несколько конфигураций: для разработки, тестов и продакшена.

    app/config.py

    Пояснения:

  • SECRET_KEY нужен Flask для подписи некоторых данных (например, сессий). В продакшене он должен быть длинным и секретным
  • DEBUG включает удобный режим разработки
  • TESTING включает режим, полезный для тестов
  • Переменные окружения и файл .env

    Чтобы не хранить секреты в репозитории, создадим шаблон:

    .env.example

    Локально вы создаёте .env (его не коммитим), а в репозиторий кладём только .env.example.

    Для удобной загрузки переменных окружения в разработке используем python-dotenv: python-dotenv на PyPI.

    Flask автоматически подхватывает .env, если python-dotenv установлен.

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

    В Flask 2+ рекомендуемый способ запуска через CLI.

    Документация: Flask Command Line Interface.

    Команда:

    Проверка:

  • откройте http://127.0.0.1:5000/health
  • ожидаемый ответ: {"status": "ok"}
  • Базовые инструменты качества кода

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

    Мы возьмём три инструмента:

  • black — автоформатирование кода (Black documentation)
  • ruff — быстрый линтер (ищет проблемы и стилистические ошибки) (Ruff documentation)
  • pytest — тестирование (pytest documentation)
  • Минимальный pyproject.toml

    Обратите внимание: pyproject.toml — общий конфигурационный файл для инструментов Python. В следующих статьях мы дополним настройки.

    Запуск:

    Автопроверки перед коммитом

    Чтобы не забывать запускать форматирование и линтер вручную, подключают pre-commit.

    Сайт проекта: pre-commit.

    На этом шаге достаточно понять идею: при попытке сделать коммит автоматически запускаются проверки, и плохой код не попадает в репозиторий.

    Итоги

    В этой статье мы:

  • выбрали архитектурные принципы для Flask-проекта
  • договорились о структуре каталогов и разделении ответственности
  • настроили виртуальное окружение и зависимости
  • сделали фабрику приложения и базовую конфигурацию
  • научились запускать приложение через Flask CLI
  • В следующей статье мы начнём наполнять каркас функциональностью: добавим первые blueprint, нормальную структуру маршрутов и подготовим основу под базу данных и тесты.

    2. Модели данных и работа с базой через SQLAlchemy

    Модели данных и работа с базой через SQLAlchemy

    В прошлой статье мы собрали каркас Flask-проекта: фабрика приложения, конфигурации, переменные окружения и базовая структура каталогов. Теперь добавим слой данных: подключим SQLAlchemy, опишем модели, настроим миграции и сделаем первые репозитории, чтобы HTTP-слой не знал деталей работы с базой.

    Что именно мы строим на этом шаге

    Мы добавим минимальную, но “боевую” основу для работы с реляционной БД:

  • подключение SQLAlchemy как расширения Flask
  • конфигурация подключения к БД через переменные окружения
  • модели User и TodoItem (типичный пример “пользователь и его сущности”)
  • миграции схемы через Flask-Migrate (Alembic)
  • репозитории для чтения и записи
  • > Мы сознательно не будем писать SQL вручную в роутерах. Роутер должен принимать запрос, вызывать сервисы или репозитории и возвращать ответ, а не собирать JOIN и думать про транзакции.

    Зависимости

    Добавим пакеты для ORM и миграций:

  • Flask-SQLAlchemy — интеграция SQLAlchemy с Flask
  • Flask-Migrate — миграции через Alembic
  • Документация:

  • Flask-SQLAlchemy
  • Flask-Migrate
  • SQLAlchemy ORM Quickstart
  • Alembic
  • requirements.txt

    Если вы планируете PostgreSQL, добавьте драйвер (в продакшене это почти всегда PostgreSQL):

    Конфигурация подключения к базе

    Мы продолжим принцип “конфигурация только через окружение”. Добавим SQLALCHEMY_DATABASE_URI.

    .env.example

    Примеры строк подключения:

  • SQLite (локально, просто стартовать): sqlite:///app.db
  • PostgreSQL: postgresql+psycopg://user:password@localhost:5432/dbname
  • app/config.py

    Подключение расширений в extensions.py

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

    app/extensions.py

  • db — точка доступа к ORM и db.session
  • migrate — интеграция миграций с Flask CLI
  • Инициализация расширений в фабрике приложения

    Теперь обновим create_app, чтобы SQLAlchemy “знал” про приложение.

    app/__init__.py

    Первая модель: пользователь

    Модель — это Python-класс, который отображается на таблицу в БД.

    Сделаем User:

  • id — первичный ключ
  • email — уникальный логин
  • password_hash — хеш пароля (не сам пароль)
  • created_at — когда пользователь создан
  • app/models/user.py

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

  • __tablename__ фиксирует имя таблицы (полезно для явности)
  • unique=True создаёт уникальный индекс на уровне БД
  • server_default=func.now() задаёт значение времени на стороне БД
  • Вторая модель: задача пользователя

    Сделаем сущность TodoItem, чтобы показать связь “один-ко-многим”:

  • один User может иметь много TodoItem
  • каждый TodoItem принадлежит одному пользователю
  • app/models/todo.py

  • ForeignKey("users.id") создаёт внешний ключ на таблицу users
  • index=True ускоряет выборку задач конкретного пользователя
  • Подключение моделей в app/models/__init__.py

    Чтобы миграции “видели” все модели, удобно импортировать их в пакете models.

    !Схема связи таблиц User и TodoItem

    Миграции: как создавать и обновлять схему базы

    Миграции — это версионируемые изменения схемы БД. Вы описываете модели, а миграции фиксируют, как изменить таблицы, чтобы они соответствовали коду.

    Подготовка CLI

    Команды миграций запускаются через Flask CLI. Убедитесь, что вы запускаете из корня проекта.

  • macOS/Linux:
  • Windows PowerShell:
  • Инициализация миграций

    Делается один раз на проект:

    Появится папка migrations/.

    Создание миграции

    После добавления или изменения моделей:

    В этот момент Alembic анализирует отличия между текущей схемой и моделями и генерирует файл миграции.

    Применение миграций

    После этого таблицы будут созданы или обновлены.

    Репозитории: изолируем доступ к данным

    Репозиторий — это слой, который инкапсулирует запросы к базе. Так вы:

  • не размазываете SQLAlchemy по роутам
  • получаете единое место для оптимизаций запросов
  • упрощаете тестирование бизнес-логики
  • Репозиторий пользователей

    #### app/repositories/user_repo.py

    Почему здесь commit внутри репозитория — допустимо:

  • в маленьком проекте это проще для старта
  • позже (когда появятся сервисы и несколько действий в одной транзакции) мы вынесем управление транзакциями выше, в слой сервисов
  • Репозиторий задач

    #### app/repositories/todo_repo.py

    Сессия и транзакции: что важно понимать на практике

    db.session в Flask-SQLAlchemy — это объект, через который вы:

  • добавляете изменения (add, delete)
  • фиксируете их (commit)
  • откатываете при ошибке (rollback)
  • Практические правила:

  • Если во время записи произошла ошибка, делайте db.session.rollback(), иначе сессия останется в “сломленном” состоянии.
  • Не держите commit в середине цепочки операций, если эти операции должны быть атомарными.
  • Позже мы оформим это аккуратнее в сервисном слое, чтобы один HTTP-запрос соответствовал одной транзакции, но для старта важно просто не забывать про rollback при исключениях.

    Типичные ошибки и как их избегать

  • Хранение пароля в чистом виде: храните только хеш.
  • Надежда на валидацию только в коде: критичные ограничения дублируйте в БД (например, unique и nullable=False).
  • Запросы из шаблонов: не делайте обращения к базе из Jinja-шаблонов, готовьте данные заранее.
  • Смешивание слоёв: роуты не должны знать, как устроены таблицы и индексы.
  • Итоги

    Теперь наш каркас из первой статьи стал “настоящим приложением” с базой:

  • подключили SQLAlchemy и миграции через расширения и фабрику приложения
  • настроили строку подключения через окружение
  • описали модели и связь “один-ко-многим”
  • научились создавать и применять миграции
  • ввели репозитории как границу доступа к данным
  • В следующей статье логично перейти к HTTP-слою поверх этих моделей: blueprint-ы, первые CRUD-ручки и базовая валидация входных данных, чтобы приложение стало полезным для пользователя и удобно расширяемым для команды.

    3. Роутинг, шаблоны и формы: веб-интерфейс приложения

    Роутинг, шаблоны и формы: веб-интерфейс приложения

    В предыдущих статьях мы:

  • собрали каркас Flask-приложения с фабрикой приложения и конфигурацией
  • подключили SQLAlchemy и миграции
  • описали модели User и TodoItem
  • добавили репозитории для изоляции работы с базой
  • Теперь построим веб-интерфейс поверх этих моделей: настроим роутинг через blueprint, научимся отдавать страницы через шаблоны и принимать данные из форм.

    Важно: механика шаблонов и форм одинаковая для любых страниц. Чтобы не упираться в разметку, в примерах ниже мы будем рендерить страницы как обычный текст (без разметки). В реальном проекте вы замените текстовые шаблоны на привычные шаблоны страниц, а серверная часть останется такой же.

    Что мы сделаем в этой статье

  • добавим blueprint web и зарегистрируем его в фабрике приложения
  • создадим набор роутов для простого сценария работы с задачами
  • подключим шаблоны и наследование шаблонов через Jinja
  • реализуем обработку форм через POST и паттерн Post/Redirect/Get
  • будем хранить user_id в сессии, чтобы веб-часть стала “состоянием” пользователя
  • !Поток данных от HTTP-запроса до базы и обратно через шаблон

    Роутинг во Flask

    Роут (маршрут) — это правило, которое связывает URL и HTTP-метод (например, GET или POST) с Python-функцией.

  • GET обычно используется для получения страницы или данных
  • POST обычно используется для отправки данных (например, формы)
  • Документация:

  • Flask Quickstart
  • Flask API: Flask.route
  • Blueprint для веб-интерфейса

    Blueprint — это способ сгруппировать роуты и связанный код по смыслу (например, web, api, auth). Так проект масштабируется без “одного огромного файла”.

    Документация:

  • Flask Blueprints
  • Создаём blueprint web

    Создадим модуль и объект blueprint.

    app/blueprints/web/__init__.py

    Здесь важно:

  • Blueprint("web", __name__) задаёт имя blueprint
  • импорт routes в конце нужен, чтобы роуты “подтянулись” при импорте пакета
  • Пишем первые роуты

    Сделаем минимальный “сценарий”:

  • вход по email (создаём пользователя, если его ещё нет)
  • просмотр списка задач
  • добавление задачи
  • переключение статуса задачи
  • app/blueprints/web/routes.py

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

  • methods=["GET", "POST"] явно разрешает форму: страницу показываем по GET, обрабатываем отправку по POST
  • request.form содержит поля формы для POST с типом application/x-www-form-urlencoded
  • session хранит данные “пользовательского состояния” между запросами
  • flash(...) кладёт короткое сообщение, которое можно показать на следующей странице
  • redirect(url_for(...)) — стандартный способ перехода после POST
  • Регистрируем blueprint в фабрике приложения

    Теперь подключим web в create_app.

    app/__init__.py

    Шаблоны и Jinja

    Шаблон — это файл, который превращается в строку ответа. Flask использует движок шаблонов Jinja.

    Документация:

  • Flask Templates
  • Jinja Template Designer Documentation
  • Где живут шаблоны

    По умолчанию Flask ищет шаблоны в папке app/templates/. Мы добавим подпапку web/.

    Рекомендуемая структура:

    Базовый шаблон и наследование

    Наследование шаблонов позволяет вынести общий каркас в один файл и переиспользовать его.

    app/templates/web/base.txt

    Здесь используются:

  • get_flashed_messages() — получение сообщений, добавленных через flash
  • {% block body %} — место, куда дочерние шаблоны подставят свой контент
  • Страница входа

    app/templates/web/login.txt

    Список задач

    app/templates/web/todos.txt

    Создание задачи

    app/templates/web/todo_new.txt

    Формы: как принимать и валидировать ввод

    Форма в веб-контексте — это способ отправить набор полей на сервер (чаще всего через POST). На стороне Flask данные доступны через request.form.

    Документация:

  • Flask API: Request
  • Минимальные правила валидации

    Валидация — это проверка входных данных до того, как вы начнёте записывать их в базу.

    В нашем примере мы сделали базовый минимум:

  • обязательность поля (email, title)
  • нормализация (strip(), lower())
  • ограничение длины title до 255 (соответствует модели)
  • проверка, что todo_id — число
  • Если валидация не проходит:

  • мы добавляем flash с причиной
  • возвращаем страницу снова с кодом 400
  • Post/Redirect/Get

    Если после POST вернуть страницу напрямую, пользователь может случайно повторно отправить форму (например, обновив страницу). Поэтому часто используют паттерн Post/Redirect/Get:

  • POST обрабатывает действие
  • затем сервер отвечает редиректом на GET
  • браузер делает GET и показывает результат
  • В коде выше это выглядит так:

  • успешный POST /todos/new заканчивается redirect(url_for("web.todos"))
  • !Паттерн PRG для безопасной обработки форм

    Сессия: простейшее состояние пользователя

    Сессия — это механизм, который позволяет “помнить” данные между запросами. Во Flask сессия по умолчанию хранится на стороне клиента в cookie, но защищена подписью.

    Ключевые моменты:

  • SECRET_KEY нужен, чтобы Flask мог подписывать данные сессии
  • в сессию кладём только небольшие данные (например, user_id), а не большие структуры
  • Документация:

  • Flask API: session
  • Репозиторий задач: расширяем под веб-сценарии

    В статье про SQLAlchemy мы сделали репозиторий для списка и создания. Для веб-интерфейса добавим точечные методы: получить задачу по id и поменять статус.

    app/repositories/todo_repo.py

    Почему set_done обновляет через update(...), а не через изменение объекта:

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

    Запуск и проверка сценария

  • Запустите приложение (как в первой статье):
  • Откройте страницу входа:
  • http://127.0.0.1:5000/login
  • Выполните вход через curl (это имитация отправки формы):
  • Флаг -c cookies.txt сохраняет cookie (а значит и сессию).

  • Посмотрите список задач:
  • Добавьте задачу:
  • Переключите статус (подставьте нужный id):
  • Итоги

    Теперь у приложения есть веб-слой поверх базы данных:

  • роутинг организован через blueprint web
  • страницы формируются шаблонами Jinja и наследованием шаблонов
  • данные форм принимаются через POST и request.form
  • для форм используется паттерн Post/Redirect/Get
  • состояние пользователя хранится в сессии через user_id
  • Следующий логичный шаг после веб-интерфейса: сделать параллельный JSON API (CRUD ручки), а затем добавить полноценную аутентификацию и авторизацию, чтобы правила доступа не были “встроены” в веб-роуты.

    4. REST API, валидация данных и обработка ошибок

    REST API, валидация данных и обработка ошибок

    В прошлых статьях мы построили основу приложения: фабрика Flask-приложения, подключили SQLAlchemy и миграции, сделали веб-интерфейс через blueprint web, шаблоны и формы. Теперь добавим параллельный слой JSON REST API, чтобы:

  • тот же проект можно было использовать с фронтендом (SPA), мобильным приложением или интеграциями
  • веб-слой и API-слой развивались независимо, но работали с одной базой и одними моделями
  • ошибки и валидация были единообразными и предсказуемыми
  • Что такое REST API в контексте Flask

    REST API — это способ организовать HTTP-эндпоинты вокруг ресурсов (например, todos) и использовать стандартные HTTP-методы:

  • GET — получить ресурс или список ресурсов
  • POST — создать ресурс
  • PATCH или PUT — частично или полностью обновить ресурс
  • DELETE — удалить ресурс
  • Важно: в реальном Flask-проекте “REST” не означает идеальную теорию, а означает понятные URL, корректные коды статуса, предсказуемые JSON-ответы и стабильные правила ошибок.

    Документация:

  • Flask Request
  • Flask Response
  • !Поток запроса в API и место валидации/обработки ошибок

    Договоримся о формате успешных ответов и ошибок

    Успешные ответы

    Для простоты и предсказуемости:

  • всегда возвращаем JSON
  • используем корректные коды статуса
  • для создания — 201 Created
  • для обычного успешного ответа — 200 OK
  • Пример успешного ответа:

    Ошибки

    В API плохо, когда ошибки “разные везде”. Поэтому введём единый формат:

  • error — короткий код ошибки (строка, удобна для фронтенда)
  • message — человекочитаемое сообщение
  • details — необязательные детали (например, ошибки полей)
  • Пример ответа ошибки:

    Зависимость для валидации: Pydantic

    Можно валидировать “вручную” через if not title, но на API это быстро становится неудобно: появляются сложные правила, типы, вложенные структуры.

    Добавим Pydantic для декларативной валидации.

  • Pydantic v2: Pydantic
  • requirements.txt

    Blueprint для API

    Сделаем отдельный blueprint api и повесим его на префикс /api. Так веб-страницы и API не будут мешать друг другу.

    app/blueprints/api/__init__.py

    Валидационные схемы (DTO) для API

    Схема — это класс, который:

  • описывает ожидаемые поля
  • проверяет типы
  • накладывает ограничения (например, длина строки)
  • app/blueprints/api/schemas.py

    Здесь важно:

  • Field(min_length=1, max_length=255) повторяет ограничения модели/БД и защищает от мусорных данных
  • входные схемы отделены от моделей SQLAlchemy: API не должно “светить” внутреннее устройство ORM
  • Единые API-ошибки через исключения

    Сделаем своё исключение ApiError, которое хранит HTTP-статус и полезные поля для JSON-ответа.

    app/api_errors.py

    Примеры того, как мы будем использовать:

  • 401 unauthorized — если пользователь не определён
  • 404 not_found — если задачи не существует или она чужая
  • 400 validation_error — если входные данные некорректные
  • Обработка ошибок централизованно

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

    app/__init__.py (добавляем обработчики)

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

  • @app.errorhandler(ApiError) делает единый JSON-формат для наших контролируемых ошибок
  • @app.errorhandler(ValidationError) превращает ошибки Pydantic в 400 и словарь ошибок по полям
  • IntegrityError откатывает транзакцию через db.session.rollback(), иначе сессия SQLAlchemy останется в некорректном состоянии
  • Извлечение пользователя в API

    Полноценную аутентификацию (пароли, токены, роли) мы сделаем позже. Сейчас нужно простое правило, чтобы API мог работать поверх наших данных.

    Мы уже используем session["user_id"] в веб-части. Для API используем то же самое: если пользователь залогинен через веб, API запросы с теми же cookie будут авторизованы.

    Сделаем маленький хелпер.

    app/blueprints/api/auth.py

    CRUD эндпоинты для Todo

    Сделаем три API-операции:

  • GET /api/todos — список задач пользователя
  • POST /api/todos — создать задачу
  • PATCH /api/todos/<id> — изменить статус is_done
  • Минимальная сериализация модели

    ORM-объект SQLAlchemy нельзя безопасно “просто вернуть”. Сделаем функцию, которая превращает TodoItem в JSON-словарь.

    app/blueprints/api/serializers.py

    Роуты API

    app/blueprints/api/routes.py

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

  • request.get_json(silent=True) возвращает None, если тело пустое или не JSON, и мы отдаём управляемую ошибку 400
  • TodoCreateIn.model_validate(payload) может бросить ValidationError, который перехватится глобальным обработчиком и превратится в единый формат validation_error
  • проверка владельца todo.user_id != user_id защищает от доступа к чужим данным
  • Content-Type, методы и коды статуса

    Чтобы клиент и сервер одинаково понимали “что мы отправляем”, полезно соблюдать правила:

  • на POST и PATCH отправляйте заголовок Content-Type: application/json
  • используйте корректные коды статуса
  • Минимальная таблица для ориентира:

    | Ситуация | Код | Когда использовать | |---|---:|---| | Успешный GET | 200 | Вернули данные | | Успешный POST (создание) | 201 | Создали ресурс | | Некорректный ввод | 400 | Неверный JSON или не прошла валидация | | Нет авторизации | 401 | Пользователь не определён | | Ресурс не найден | 404 | Нет такой сущности или нет доступа | | Конфликт данных | 409 | Нарушены ограничения БД (например, уникальность) |

    Проверка через curl

    Ниже пример сценария, который использует веб-логин (сессия cookie) и затем ходит в /api.

  • Войти и сохранить cookie:
  • Создать задачу через API:
  • Получить список:
  • Обновить статус:
  • Типичные ошибки при разработке API

  • Возвращать разные форматы ошибок в разных местах: лучше один формат и общий обработчик.
  • Не проверять Content-Type и не обрабатывать “не JSON”: в итоге сервер падает или отдаёт HTML-страницу.
  • Смешивать ORM-модели и контракты API: модели меняются, а API-контракт должен быть стабильнее.
  • Не делать rollback() на ошибках БД: после исключения сессия SQLAlchemy может перестать работать до отката.
  • Итоги

    Теперь у проекта есть полноценный API-слой:

  • отдельный blueprint api на /api
  • входные данные валидируются через Pydantic
  • ошибки приводятся к единому JSON-формату через централизованные error handlers
  • CRUD-операции используют репозитории и не смешивают HTTP-слой с деталями SQLAlchemy
  • Следующий логичный шаг для “боевого” приложения: добавить полноценную аутентификацию и авторизацию (пароли, хеширование, токены), а затем тесты для API-ручек, чтобы контракт и ошибки не ломались при изменениях.

    5. Аутентификация, авторизация и безопасность приложения

    Аутентификация, авторизация и безопасность приложения

    В прошлых статьях мы сделали рабочий Flask-проект: база через SQLAlchemy, веб-интерфейс, JSON API, валидация через Pydantic и единые обработчики ошибок. Но вход в веб-часть у нас был учебным: пользователь создавался по email, а в сессии хранился user_id. Это удобно для старта, но небезопасно.

    Теперь доведём проект до состояния, близкого к продакшену:

  • сделаем аутентификацию по паролю с безопасным хранением
  • введём авторизацию как правило доступа к данным
  • добавим API-токены для интеграций
  • настроим базовые параметры безопасности для cookie-сессий и обработку типовых угроз
  • !Две основные модели аутентификации: cookie-сессия для веба и Bearer-токен для API

    Термины: аутентификация и авторизация

  • Аутентификация отвечает на вопрос: кто вы?
  • - пример: вход по email и паролю - результат: сервер получает подтверждённую личность пользователя
  • Авторизация отвечает на вопрос: что вам можно?
  • - пример: пользователь может менять только свои задачи TodoItem, а не чужие - результат: сервер либо разрешает действие, либо запрещает

    Эти понятия часто путают, но в коде их полезно держать раздельно:

  • аутентификация извлекает пользователя (или user_id) из сессии/токена
  • авторизация проверяет право доступа к конкретному ресурсу
  • Безопасное хранение паролей

    Главное правило: никогда не хранить пароль в чистом виде.

    Вместо этого хранится password_hash, который получают из пароля специальной функцией хеширования паролей. Во Flask-проектах для этого обычно используют утилиты Werkzeug:

  • generate_password_hash(password)
  • check_password_hash(hash, password)
  • Документация: Werkzeug Password Hashing

    Обновляем модель пользователя

    В прошлой реализации мы заполняли password_hash="demo". Теперь сделаем нормальные методы установки и проверки пароля.

    app/models/user.py

    Практические замечания:

  • generate_password_hash сам выбирает безопасный формат и включает соль
  • check_password_hash сравнивает корректно и безопасно
  • Регистрация и логин для веб-части

    Мы сохраним идею cookie-сессии: после успешного входа кладём user_id в session. Это хорошо подходит для браузера.

    Документация: Flask session

    Сервис аутентификации

    Чтобы не писать логику хеширования паролей прямо в роуте, вынесем её в сервис.

    app/services/auth_service.py

    Роуты аутентификации

    Сделаем отдельный blueprint auth, чтобы не смешивать страницы задач и вход.

    app/blueprints/auth/__init__.py

    app/blueprints/auth/routes.py

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

  • мы не сообщаем, существует ли email в системе, отдельной фразой. Сообщение общее уменьшает утечки информации
  • минимальная политика паролей: длина 8+. В реальном проекте часто добавляют проверку на популярные пароли
  • Шаблоны для auth

    app/templates/auth/register.txt

    app/templates/auth/login.txt

    Регистрация blueprint в фабрике

    app/__init__.py

    Авторизация на примере задач

    У нас уже есть правило: пользователь может видеть и менять только свои задачи. Это и есть авторизация на уровне ресурса.

    В API мы делали проверку:

  • найти задачу
  • сравнить todo.user_id и текущий user_id
  • Это правильный минимум. Тонкость в том, что такие проверки лучше стандартизировать: либо в сервисах, либо в общих функциях, чтобы не забывать.

    Практический подход для проекта этого масштаба:

  • в каждом эндпоинте, который работает с TodoItem, делать проверку владельца
  • возвращать 404, а не 403, чтобы не помогать злоумышленнику угадывать идентификаторы чужих сущностей
  • API-токены для интеграций

    Cookie-сессия хороша для браузера, но для интеграций удобнее токены:

  • клиент отправляет заголовок Authorization: Bearer <token>
  • сервер ищет токен и определяет пользователя
  • Важно: токен нельзя хранить в базе в открытом виде. Если база утечёт, утекут и все токены.

    Модель токена

    Добавим таблицу, где хранится только хеш токена.

    app/models/api_token.py

    И добавим импорт в app/models/__init__.py:

    Затем миграции:

    Репозиторий токенов

    app/repositories/token_repo.py

    Генерация токена

    Токен генерируем случайным образом, показываем клиенту один раз, а в базе сохраняем только хеш.

    app/services/token_service.py

    Документация:

  • secrets.token_urlsafe
  • hashlib.sha256
  • Аутентификация API по Bearer-токену

    Обновим require_user_id() так, чтобы он сначала пробовал токен, и только потом сессию (удобно для разработки).

    app/blueprints/api/auth.py

    Эндпоинт выдачи токена

    Сделаем эндпоинт, который выдаёт токен только после проверки пароля.

    app/blueprints/api/routes_tokens.py

    И не забудьте импортировать роуты токенов, чтобы Flask их зарегистрировал. Например, в app/blueprints/api/__init__.py оставить импорт routes, а в app/blueprints/api/routes.py добавить:

    Проверка:

  • Получить токен:
  • Использовать токен:
  • Настройки безопасности для cookie-сессий

    Если вы используете cookie-сессию (как мы в веб-части), важно настроить cookie так, чтобы уменьшить риск кражи.

    В Flask это делается через конфигурацию.

    Документация: Flask configuration

    Рекомендуемые параметры

    app/config.py

    Что это даёт:

  • SESSION_COOKIE_HTTPONLY=True снижает риск кражи cookie через вредоносный JavaScript
  • SESSION_COOKIE_SAMESITE="Lax" уменьшает риск CSRF для части сценариев, сохраняя удобство обычной навигации
  • SESSION_COOKIE_SECURE=True в продакшене означает, что cookie отправляется только по HTTPS
  • CSRF: когда нужно и что делать

    CSRF это атака, при которой браузер пользователя отправляет запрос на ваш сайт без намерения пользователя, потому что браузер автоматически прикладывает cookie.

    Важно:

  • CSRF актуален, когда аутентификация основана на cookie (сессия)
  • CSRF менее актуален для чистого API по Bearer-токену, если токен не хранится в cookie и не отправляется браузером автоматически
  • Практика:

  • для всех изменяющих действий в веб-интерфейсе (POST, PATCH, DELETE) включать CSRF-защиту
  • популярный путь во Flask: использовать расширение Flask-WTF
  • Документация: Flask-WTF

    Если вы пока не используете HTML-формы (как в наших текстовых шаблонах и curl), CSRF в учебном режиме можно отложить, но при переходе на реальный браузерный интерфейс это нужно включить обязательно.

    Другие базовые меры безопасности

    Ниже практичный минимум для типового Flask-сервиса.

  • Всегда использовать HTTPS в продакшене
  • - иначе cookie и токены могут быть перехвачены в сети
  • Ограничивать попытки логина
  • - защита от перебора паролей - часто делают через rate limit на уровне reverse proxy или через расширения
  • Аккуратно логировать
  • - не писать в логи пароли, токены, содержимое Authorization
  • Разделять секреты и код
  • - SECRET_KEY, строка подключения к БД и другие секреты только через переменные окружения

    Полезные источники:

  • Flask Security Considerations
  • OWASP Top 10
  • Итоги

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

  • веб-вход работает по паролю с безопасным хешированием
  • введены понятные границы: аутентификация определяет пользователя, авторизация проверяет доступ к ресурсам
  • добавлены API-токены по схеме Bearer, при этом в базе хранится только хеш токена
  • включены базовые настройки безопасности cookie-сессий и разобрано, где нужен CSRF
  • Следующий логичный шаг для продакшена: покрыть критические сценарии тестами (логин, выдача токена, запреты доступа), а также настроить деплой с HTTPS и секретами окружения.

    6. Тестирование, отладка и наблюдаемость (логирование, метрики)

    Тестирование, отладка и наблюдаемость (логирование, метрики)

    На предыдущих шагах мы собрали реальный Flask-проект с фабрикой приложения, базой через SQLAlchemy, веб-частью, REST API, валидацией и безопасной аутентификацией (cookie-сессии для веба и Bearer-токены для API). Теперь доведём проект до уровня, на котором его можно уверенно развивать в команде и сопровождать в продакшене.

    Эта статья закрывает три практические задачи:

  • Тестирование: быстро ловить регрессии и фиксировать контракт API.
  • Отладка: ускорять поиск причин ошибок локально и на стендах.
  • Наблюдаемость: понимать, что происходит в приложении в реальном времени через логи и метрики.
  • !Пирамида тестов помогает выбрать, какие тесты писать чаще всего

    Что считать хорошим тестовым набором

    Цель тестов в прикладном Flask-проекте:

  • проверять бизнес-правила (например, «задачу можно менять только владельцу»)
  • фиксировать формат ошибок API (например, единый validation_error)
  • ловить поломки при рефакторинге (например, изменение репозитория не должно ломать сервис)
  • Практичный ориентир — пирамида тестов:

  • Юнит-тесты: проверяют маленький кусок логики без реального HTTP и без реальной БД (или с очень контролируемой средой).
  • Интеграционные тесты: проверяют несколько слоёв вместе (например, API-эндпоинт + БД) через Flask test client.
  • E2E: прогоняют сценарий почти как пользователь (обычно реже, часто отдельно в CI).
  • В рамках курса основной упор делаем на интеграционные тесты через test client, потому что они дают лучшую отдачу: проверяют HTTP, валидацию, ошибки и слой данных вместе.

    Инструменты

  • pytest — тестовый раннер и экосистема фикстур: pytest documentation
  • встроенный Flask test client для запросов без реального сервера: Flask Testing
  • SQLAlchemy/Flask-SQLAlchemy для тестовой базы и транзакций: Flask-SQLAlchemy
  • Добавьте (если ещё не добавляли) в requirements-dev.txt:

    Организация тестов в проекте

    Рекомендуемая структура (минимум):

    conftest.py — место, где живут общие фикстуры pytest.

    Тестовое приложение и тестовый клиент

    Ключевой паттерн из первых статей — фабрика create_app() — делает тестирование простым: на каждый тест можно создавать чистое приложение со специальной конфигурацией.

    Конфигурация для тестов

    У нас уже был TestingConfig с sqlite:///:memory:. Для тестов важны ещё два момента:

  • TESTING = True — включает тестовый режим Flask
  • отключение CSRF (если вы подключите Flask-WTF позже)
  • Пример (если нужно дополнить):

    Фикстуры pytest: приложение, клиент и база

    Сделаем фикстуры:

  • app — Flask-приложение в режиме тестирования
  • client — Flask test client
  • db_setup — создание и очистка таблиц
  • Замечания:

  • autouse=True означает, что база будет подниматься для каждого теста автоматически.
  • На больших проектах часто используют транзакции и rollback, но для учебного проекта create_all/drop_all на SQLite достаточно прозрачен и надёжен.
  • Пишем первые тесты: smoke и контракт ошибок

    Smoke-тест здоровья сервиса

    Этот тест кажется простым, но он полезен:

  • проверяет, что приложение собирается
  • проверяет базовый роутинг и JSON
  • Тестируем единый формат ошибок API

    В статье про API мы договорились про формат:

  • error
  • message
  • details (опционально)
  • Зафиксируем это тестом на случай, если в будущем кто-то “случайно” вернёт HTML или другой формат.

    Тестирование API со входом: cookie-сессия

    В нашем проекте API может авторизоваться через cookie-сессию (если запросы идут от браузера). В тестах мы можем установить session["user_id"] напрямую.

    Создание пользователя в базе для теста

    Чтобы не зависеть от веб-роутов регистрации, в тестах можно создать пользователя напрямую через SQLAlchemy.

    Что здесь проверяется:

  • валидация входного JSON (через json=... test client выставляет корректный Content-Type)
  • код статуса 201 при создании
  • корректная сериализация
  • Тест валидации Pydantic

    Этот тест защищает ключевую практику: входные данные всегда валидируются и всегда дают предсказуемую ошибку.

    Тестирование API с Bearer-токеном

    В статье про безопасность мы добавили Bearer-токены (Authorization: Bearer <token>). Для интеграционных тестов удобнее всего:

  • создать пользователя
  • запросить токен через /api/tokens
  • вызвать защищённый эндпоинт с заголовком Authorization
  • Плюс этого подхода: тест покрывает сразу несколько слоёв — роуты, сервисы, репозитории, хеширование токена, обработку заголовков.

    Отладка: как быстрее находить причину проблемы

    Локальная отладка Flask

    В режиме разработки запускайте так:

    Режим --debug полезен тем, что:

  • показывает трассировку исключения
  • автоматически перезапускает сервер при изменениях кода
  • Документация: Flask Debug Mode

    Отладка тестов

    Полезные приёмы:

  • запуск одного теста:
  • показать больше деталей:
  • остановка на первой ошибке:
  • временный вывод в консоль (в pytest по умолчанию вывод может “прятаться”):
  • Типовая ошибка SQLAlchemy: забыли rollback

    Если в приложении случился IntegrityError (например, уникальность email), сессия SQLAlchemy может оставаться в состоянии ошибки. Мы уже добавляли обработчик, который делает db.session.rollback().

    Практическое правило:

  • если увидели ошибки вида “This Session's transaction has been rolled back due to a previous exception”, значит где-то после исключения не случился rollback().
  • Наблюдаемость: логи и метрики

    Наблюдаемость — это практики и инструменты, которые помогают ответить на вопросы:

  • Что сломалось?
  • У скольких пользователей?
  • Это баг или деградация производительности?
  • С какого момента началось?
  • Классический минимум — логи и метрики. Трейсинг часто добавляют позже.

    !Три сигнала наблюдаемости и что каждый даёт

    Логирование: минимальный продакшен-подход

    Почему print() недостаточно

    print():

  • плохо управляется по уровням важности
  • неудобно собирать централизованно
  • не даёт структуры (например, request id)
  • Вместо этого используем стандартный модуль logging.

    Документация: Python logging

    Настройка логирования в фабрике

    Сделаем простую конфигурацию:

  • уровень логов зависит от режима
  • формат содержит время, уровень, модуль и сообщение
  • Подключим в create_app():

    Request ID: связываем логи одного запроса

    Если у вас много запросов, полезно уметь собрать логи конкретного запроса. Минимальный приём — request_id.

    Подключение:

    Замечания:

  • В продакшене часто request id задаёт reverse proxy (например, Nginx) и прокидывает в X-Request-Id.
  • В “идеале” формат логов делают JSON, чтобы лог-система (ELK, Loki и т.д.) парсила поля. Для этого часто используют structlog: structlog documentation
  • Метрики: Prometheus-стиль

    Метрики отвечают на вопросы, на которые логи отвечают хуже:

  • сколько запросов в секунду
  • какая задержка на 95-м перцентиле
  • сколько ошибок 500 за последние 5 минут
  • Для простоты подключим prometheus_client и сделаем /metrics.

    Проект: Prometheus Python client

    Зависимость

    Добавьте в requirements.txt или отдельный requirements-observability.txt:

    Экспорт метрик и базовые метрики HTTP

    Сделаем:

  • Counter на количество запросов
  • Histogram на длительность запросов
  • /metrics эндпоинт
  • Подключение в фабрике:

    Практические замечания:

  • Не размечайте path слишком детально, если в пути есть идентификаторы (/api/todos/123). В продакшене обычно нормализуют пути (например, /api/todos/<id>), иначе кардинальность метрик становится слишком большой.
  • /metrics должен быть доступен вашему мониторингу, но часто закрывается от публичного интернета.
  • Как всё это использовать в продакшене

    Минимальные рекомендации:

  • Логи пишите в stdout/stderr — контейнеры и оркестраторы (Docker/Kubernetes) умеют их собирать.
  • Метрики отдавайте на /metrics и собирайте Prometheus-совместимым скрейпером.
  • Ошибки уровня 500 обязательно логируйте с трассировкой исключения.
  • Дополнительно (если вы хотите следующий шаг):

  • распределённый трейсинг через OpenTelemetry: OpenTelemetry for Python
  • Итоги

    Теперь проект “Flask от идеи до продакшена” получил то, что обычно отличает учебный код от рабочего:

  • интеграционные тесты через Flask test client и pytest
  • тесты на контракты ошибок и аутентификацию (cookie-сессия и Bearer-токен)
  • практичные приёмы отладки (локально и в тестах)
  • минимально достаточную наблюдаемость: логирование с request id и метрики в стиле Prometheus
  • 7. Деплой и продакшен: Docker, конфигурации и CI/CD

    Деплой и продакшен: Docker, конфигурации и CI/CD

    К этому моменту у нас есть полноценное Flask-приложение: база через SQLAlchemy и миграции, веб-часть, REST API с валидацией и едиными ошибками, аутентификация (cookie-сессии для веба и Bearer-токены для API), тесты и базовая наблюдаемость (логи, метрики). Теперь цель простая: превратить проект в воспроизводимый и безопасный сервис, который можно стабильно запускать на сервере и обновлять без ручной магии.

    В этой статье мы соберём практический продакшен-пайплайн:

  • упакуем приложение в Docker-образ
  • поднимем окружение локально через Docker Compose (приложение + PostgreSQL)
  • зафиксируем правила конфигурации для dev/test/prod
  • настроим CI (проверки качества и тесты)
  • добавим шаги CD (сборка и публикация образа, готовность к деплою)
  • !Общая картина продакшен-архитектуры и поток запросов

    Что значит продакшен для Flask-проекта

    В разработке мы запускали сервер через flask --debug run. В продакшене так делать нельзя, потому что dev-сервер Flask не предназначен для нагрузки и безопасной эксплуатации.

    В продакшене обычно используется связка:

  • WSGI/ASGI-сервер (для Flask это чаще всего WSGI): Gunicorn
  • reverse proxy (часто Nginx): TLS (HTTPS), отдача статики, лимиты, буферизация
  • отдельная база данных: PostgreSQL
  • Полезные ссылки:

  • Deploying to Production
  • Gunicorn documentation
  • Docker Documentation
  • Конфигурации: один код, разные окружения

    Мы уже используем классы конфигурации и переменные окружения. В продакшене важно закрепить правило:

  • код один и тот же, меняется только конфигурация
  • секреты не лежат в репозитории, а приходят через окружение (CI secrets, секрет-хранилище, переменные платформы)
  • Добавляем явный выбор конфигурации

    Сделаем так, чтобы приложение выбирало конфигурацию через переменную окружения. Добавим файл wsgi.py в корень проекта.

    Теперь у нас есть стабильная точка входа wsgi:app для Gunicorn.

    Минимальный набор продакшен-переменных

    Договоримся о переменных окружения:

  • FLASK_CONFIG — какая конфигурация применяется
  • SECRET_KEY — секрет подписи cookie-сессий
  • SQLALCHEMY_DATABASE_URI — строка подключения к базе
  • Пример .env.example для контейнерного запуска:

    Важно:

  • SECRET_KEY в продакшене должен быть длинным и случайным
  • база в продакшене почти всегда PostgreSQL
  • Docker: делаем образ приложения воспроизводимым

    Задача Docker здесь практичная: собрать один артефакт, который одинаково работает локально, на CI и на сервере.

    .dockerignore

    Чтобы не тащить в образ мусор и секреты:

    Dockerfile

    Сделаем образ на базе slim, установим зависимости, добавим пользователя без root и будем запускать Gunicorn.

    Пояснения:

  • PYTHONUNBUFFERED=1 делает логи предсказуемыми в контейнере
  • gunicorn -w 2 — два воркера как безопасный старт (позже подбирается по CPU/памяти)
  • запуск под non-root уменьшает ущерб при компрометации
  • Docker Compose: приложение + PostgreSQL

    Compose нужен для двух сценариев:

  • локально поднять окружение максимально близкое к продакшену
  • использовать тот же подход на тестовых серверах
  • docker-compose.yml

    После этого у вас будет готовый артефакт ghcr.io/<owner>/<repo>:latest, который можно запускать на сервере.

    Практика запуска на сервере

    Минимальный вариант: VPS + Docker Compose

    Типовой процесс:

  • Установить Docker на сервер.
  • Скопировать docker-compose.yml.
  • Создать .env с SECRET_KEY и параметрами базы.
  • Запустить docker compose up -d.
  • Документация установки Docker:

  • Install Docker Engine
  • Reverse proxy и HTTPS

    В реальном продакшене почти всегда нужен HTTPS и нормальный входной слой. Обычно это решается Nginx или другим reverse proxy перед Gunicorn.

    Что делает reverse proxy:

  • завершает TLS (HTTPS)
  • может ограничивать размер тела запроса
  • может добавлять заголовки безопасности
  • может проксировать /metrics только для внутренней сети
  • Если вы используете managed-платформы (например, облачные контейнер-сервисы), reverse proxy может быть встроен.

    Чек-лист продакшен-готовности

    Перед тем как считать деплой нормальным, проверьте:

  • Секреты
  • - SECRET_KEY не дефолтный - токены, пароли и DSN базы не логируются
  • База и миграции
  • - миграции применяются контролируемо - db.session.rollback() делается на ошибках (мы уже добавляли это в обработчике IntegrityError)
  • Здоровье и наблюдаемость
  • - есть /health - логи пишутся в stdout - метрики доступны там, где нужно, и закрыты от публичного доступа
  • Безопасность cookie
  • - SESSION_COOKIE_SECURE=True в продакшене - SESSION_COOKIE_HTTPONLY=True - SESSION_COOKIE_SAMESITE настроен

    Итоги

    Теперь проект действительно готов к продакшен-жизни:

  • есть корректная точка входа wsgi:app и запуск через Gunicorn
  • приложение упаковано в Docker-образ
  • окружение поднимается через Docker Compose с PostgreSQL
  • конфигурация управляется переменными окружения
  • CI прогоняет линтеры и тесты на каждый push/PR
  • CD собирает и публикует Docker-образ в registry, готовый к деплою