Home » Как структурировать большое Flask-приложение с Blueprints и SQLAlchemy
Как структурировать большое Flask-приложение с Blueprints и SQLAlchemy

Как структурировать большое Flask-приложение с Blueprints и SQLAlchemy

Если вы когда-нибудь пытались развернуть Flask-приложение на продакшене, то знаете, что структура проекта может быстро превратиться в кашу из моделей, вьюшек и конфигов. Особенно когда приложение растет и нужно поддерживать десятки роутов, сотни моделей и множество модулей. Именно поэтому грамотная архитектура с использованием Blueprints и SQLAlchemy — это не просто “хорошая практика”, а необходимость для любого серьезного проекта.

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

Анатомия правильной структуры Flask-приложения

Давайте сразу посмотрим на идеальную структуру проекта, которую я использую в 90% случаев:

myapp/
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── extensions.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── post.py
│   │   └── base.py
│   ├── auth/
│   │   ├── __init__.py
│   │   ├── routes.py
│   │   ├── forms.py
│   │   └── utils.py
│   ├── blog/
│   │   ├── __init__.py
│   │   ├── routes.py
│   │   ├── forms.py
│   │   └── utils.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── v1/
│   │   │   ├── __init__.py
│   │   │   ├── users.py
│   │   │   └── posts.py
│   │   └── v2/
│   ├── templates/
│   ├── static/
│   └── utils/
├── migrations/
├── tests/
├── requirements.txt
├── config.py
├── run.py
└── wsgi.py

Эта структура решает три основные проблемы:

  • Модульность — каждый компонент живет в своем namespace
  • Масштабируемость — легко добавлять новые функции без переписывания существующего кода
  • Тестируемость — каждый модуль можно тестировать независимо

Настройка базового приложения с Application Factory

Первым делом создаем Application Factory pattern в app/__init__.py:

from flask import Flask
from flask_migrate import Migrate
from flask_login import LoginManager

from app.extensions import db, ma, jwt
from app.config import Config

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)
    
    # Инициализация расширений
    db.init_app(app)
    ma.init_app(app)
    jwt.init_app(app)
    
    # Регистрация blueprints
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    from app.blog import bp as blog_bp
    app.register_blueprint(blog_bp, url_prefix='/blog')
    
    from app.api.v1 import bp as api_v1_bp
    app.register_blueprint(api_v1_bp, url_prefix='/api/v1')
    
    # Основной роут
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    return app

В файле app/extensions.py выносим все расширения:

from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_jwt_extended import JWTManager
from flask_migrate import Migrate
from flask_login import LoginManager

db = SQLAlchemy()
ma = Marshmallow()
jwt = JWTManager()
migrate = Migrate()
login_manager = LoginManager()

Создание и настройка Blueprints

Теперь создадим первый blueprint для аутентификации. В файле app/auth/__init__.py:

from flask import Blueprint

bp = Blueprint('auth', __name__)

from app.auth import routes

И соответствующие роуты в app/auth/routes.py:

from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash

from app.auth import bp
from app.models.user import User
from app.extensions import db

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        
        user = User.query.filter_by(username=username).first()
        
        if user and check_password_hash(user.password_hash, password):
            login_user(user)
            return redirect(url_for('main.index'))
        
        flash('Invalid username or password')
    
    return render_template('auth/login.html')

@bp.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('main.index'))

Модели SQLAlchemy: базовый класс и наследование

Создаем базовую модель в app/models/base.py:

from datetime import datetime
from app.extensions import db

class BaseModel(db.Model):
    __abstract__ = True
    
    id = db.Column(db.Integer, primary_key=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def save(self):
        """Сохранить объект в базу данных"""
        db.session.add(self)
        db.session.commit()
    
    def delete(self):
        """Удалить объект из базы данных"""
        db.session.delete(self)
        db.session.commit()
    
    def to_dict(self):
        """Преобразовать объект в словарь"""
        return {c.name: getattr(self, c.name) for c in self.__table__.columns}

Теперь создаем модель пользователя в app/models/user.py:

from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash

from app.extensions import db
from app.models.base import BaseModel

class User(UserMixin, BaseModel):
    __tablename__ = 'users'
    
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128))
    is_active = db.Column(db.Boolean, default=True)
    is_admin = db.Column(db.Boolean, default=False)
    
    # Связи
    posts = db.relationship('Post', backref='author', lazy='dynamic')
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f''

Конфигурация приложения

В файле app/config.py настраиваем разные окружения:

import os
from datetime import timedelta

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
    
    # База данных
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ENGINE_OPTIONS = {
        'pool_pre_ping': True,
        'pool_recycle': 300,
    }
    
    # JWT
    JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or SECRET_KEY
    JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
    JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
    
    # Пагинация
    POSTS_PER_PAGE = 10
    
    # Загрузка файлов
    UPLOAD_FOLDER = 'app/static/uploads'
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16MB max file size

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///app_dev.db'

class ProductionConfig(Config):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://user:pass@localhost/myapp'
    
    # Логирование
    LOG_LEVEL = 'INFO'

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
    WTF_CSRF_ENABLED = False

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}

API Blueprint с версионированием

Создаем API blueprint в app/api/v1/__init__.py:

from flask import Blueprint

bp = Blueprint('api_v1', __name__)

from app.api.v1 import users, posts

И API для пользователей в app/api/v1/users.py:

from flask import jsonify, request
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity

from app.api.v1 import bp
from app.models.user import User
from app.extensions import db

@bp.route('/users', methods=['GET'])
@jwt_required()
def get_users():
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    users = User.query.paginate(
        page=page, 
        per_page=per_page, 
        error_out=False
    )
    
    return jsonify({
        'users': [user.to_dict() for user in users.items],
        'pagination': {
            'page': page,
            'pages': users.pages,
            'per_page': per_page,
            'total': users.total
        }
    })

@bp.route('/users/', methods=['GET'])
@jwt_required()
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify(user.to_dict())

@bp.route('/users', methods=['POST'])
def create_user():
    data = request.get_json()
    
    if User.query.filter_by(username=data['username']).first():
        return jsonify({'error': 'Username already exists'}), 400
    
    user = User(
        username=data['username'],
        email=data['email']
    )
    user.set_password(data['password'])
    user.save()
    
    access_token = create_access_token(identity=user.id)
    
    return jsonify({
        'user': user.to_dict(),
        'access_token': access_token
    }), 201

Миграции и развертывание

Создаем точку входа в run.py:

import os
from app import create_app, db
from app.models import User, Post
from flask_migrate import upgrade

app = create_app(os.getenv('FLASK_CONFIG') or 'default')

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post': Post}

@app.cli.command()
def deploy():
    """Команда для развертывания на продакшене"""
    # Создание миграций
    upgrade()
    
    # Создание админа, если его нет
    admin = User.query.filter_by(username='admin').first()
    if not admin:
        admin = User(
            username='admin',
            email='admin@example.com',
            is_admin=True
        )
        admin.set_password('change-me-in-production')
        admin.save()
        print('Admin user created')

if __name__ == '__main__':
    app.run(debug=True)

Инициализируем миграции:

export FLASK_APP=run.py
flask db init
flask db migrate -m "Initial migration"
flask db upgrade

Сравнение подходов к структурированию

Подход Плюсы Минусы Когда использовать
Monolithic structure Простота, быстрый старт Плохо масштабируется, код превращается в спагетти Прототипы, малые проекты
Blueprints Модульность, версионирование API, легкое тестирование Немного сложнее для новичков Средние и большие проекты
Flask-RESTX Автодокументация, валидация, сериализация Больше boilerplate кода API-first приложения
FastAPI Автодокументация, type hints, высокая производительность Другой фреймворк, меньше middleware Новые высоконагруженные API

Оптимизация для production

Создаем wsgi.py для развертывания:

import os
from app import create_app

app = create_app(os.getenv('FLASK_CONFIG') or 'production')

if __name__ == "__main__":
    app.run()

И requirements.txt:

Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-Migrate==4.0.5
Flask-Login==0.6.2
Flask-JWT-Extended==4.5.2
Flask-Marshmallow==0.15.0
marshmallow-sqlalchemy==0.29.0
psycopg2-binary==2.9.7
gunicorn==21.2.0
python-dotenv==1.0.0

Деплой на VPS

Скрипт для автоматического деплоя:

#!/bin/bash
# deploy.sh

# Создание пользователя и директории
sudo useradd -m -s /bin/bash myapp
sudo mkdir -p /var/www/myapp
sudo chown myapp:myapp /var/www/myapp

# Клонирование и настройка
cd /var/www/myapp
git clone https://github.com/yourusername/myapp.git .
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Переменные окружения
cat > .env << EOF FLASK_CONFIG=production SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") DATABASE_URL=postgresql://myapp:password@localhost/myapp JWT_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") EOF # Настройка PostgreSQL sudo -u postgres psql -c "CREATE DATABASE myapp;" sudo -u postgres psql -c "CREATE USER myapp WITH PASSWORD 'password';" sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp;" # Миграции flask db upgrade flask deploy # Systemd service sudo tee /etc/systemd/system/myapp.service > /dev/null <

Мониторинг и логирование

Добавляем логирование в app/__init__.py:

import logging
from logging.handlers import RotatingFileHandler
import os

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)
    
    # ... инициализация расширений
    
    # Настройка логирования
    if not app.debug and not app.testing:
        if not os.path.exists('logs'):
            os.mkdir('logs')
        
        file_handler = RotatingFileHandler(
            'logs/myapp.log', 
            maxBytes=10240000, 
            backupCount=10
        )
        file_handler.setFormatter(logging.Formatter(
            '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
        ))
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)
        
        app.logger.setLevel(logging.INFO)
        app.logger.info('MyApp startup')
    
    return app

Интересные фишки и продвинутые техники

Кастомные CLI команды для администрирования:

import click
from flask.cli import with_appcontext

@click.command()
@click.option('--count', default=100, help='Number of fake users to create')
@with_appcontext
def create_fake_users(count):
    """Создать фейковых пользователей для тестирования"""
    from faker import Faker
    fake = Faker()
    
    for i in range(count):
        user = User(
            username=fake.user_name(),
            email=fake.email()
        )
        user.set_password('password')
        user.save()
    
    click.echo(f'Created {count} fake users')

# Регистрация команды
app.cli.add_command(create_fake_users)

Middleware для профилирования:

from flask import g
import time

@app.before_request
def before_request():
    g.start_time = time.time()

@app.after_request
def after_request(response):
    if hasattr(g, 'start_time'):
        runtime = time.time() - g.start_time
        response.headers['X-Runtime'] = f'{runtime:.3f}s'
    return response

Автоматизация и CI/CD

GitHub Actions для автоматического деплоя:

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to server
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.KEY }}
        script: |
          cd /var/www/myapp
          git pull origin main
          source venv/bin/activate
          pip install -r requirements.txt
          flask db upgrade
          sudo systemctl restart myapp
          sudo systemctl restart nginx

Альтернативы и похожие решения

Если Flask кажется слишком простым, стоит рассмотреть:

  • Django — полноценный фреймворк с ORM, админкой и кучей встроенных фич
  • FastAPI — современный фреймворк с автодокументацией и type hints
  • Starlette — легковесный ASGI фреймворк
  • Quart — асинхронный аналог Flask

Для структурирования проектов также полезны:

  • Flask-RESTX — для API с автодокументацией
  • Flask-Admin — готовая админка
  • Celery — для фоновых задач
  • Flask-Caching — кеширование

Официальная документация: https://flask.palletsprojects.com/

SQLAlchemy документация: https://docs.sqlalchemy.org/

Заключение и рекомендации

Правильная структура Flask-приложения — это не просто “хорошая практика”, а необходимость для любого проекта, который планируется развивать дольше месяца. Blueprints позволяют легко масштабировать функциональность, а Application Factory pattern упрощает тестирование и развертывание.

Когда использовать эту структуру:

  • Проекты с более чем 10 роутами
  • Командная разработка
  • Приложения с API и веб-интерфейсом
  • Проекты с планами на рост

Основные преимущества:

  • Код легко читается и поддерживается
  • Простое добавление новых функций
  • Изолированное тестирование модулей
  • Удобное развертывание на любом хостинге

Для небольших проектов эта структура может показаться избыточной, но поверьте — лучше потратить час на правильную архитектуру в начале, чем дни на рефакторинг потом. А если вы планируете деплой на продакшен, то VPS с PostgreSQL и правильно настроенным gunicorn — это минимум, который нужен для стабильной работы.


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

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

Leave a reply

Your email address will not be published. Required fields are marked