Разработка бэкенда сайта на Python (Flask)
Flask остаётся актуальным не потому что устарел, а потому что правильно расставляет ответственности. Это микрофреймворк: он даёт HTTP-маршрутизацию, request/response контекст и ничего лишнего. ORM, сериализацию, аутентификацию, кеширование — выбираете и собираете сами. Это слабость для новичков и сила для опытных команд, которые хотят контроль.
Flask хорошо подходит для: прототипов, небольших API, сервисов с нестандартной логикой, проектов где Django избыточен, а FastAPI — оверинжиниринг.
Application Factory и Blueprints
Правильная инициализация Flask — через фабрику. Это позволяет создавать несколько экземпляров с разными конфигурациями (для тестов особенно важно):
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_jwt_extended import JWTManager
from flask_caching import Cache
db = SQLAlchemy()
migrate = Migrate()
jwt = JWTManager()
cache = Cache()
def create_app(config_name: str = 'development') -> Flask:
app = Flask(__name__)
app.config.from_object(config[config_name])
db.init_app(app)
migrate.init_app(app, db)
jwt.init_app(app)
cache.init_app(app)
# Регистрация Blueprint
from .api.v1 import bp as api_v1
app.register_blueprint(api_v1, url_prefix='/api/v1')
from .auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/api/auth')
return app
Blueprint изолирует группу маршрутов:
# app/api/v1/products.py
from flask import Blueprint, request, jsonify, abort
from ..models import Product
from ..extensions import db, cache
from .decorators import require_auth, require_role
bp = Blueprint('products', __name__)
@bp.get('/products')
@cache.cached(timeout=300, query_string=True)
def list_products():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
category_id = request.args.get('category_id', type=int)
query = Product.query.filter_by(is_active=True)
if category_id:
query = query.filter_by(category_id=category_id)
pagination = query.order_by(Product.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
'data': [p.to_dict() for p in pagination.items],
'pagination': {
'page': pagination.page,
'pages': pagination.pages,
'total': pagination.total
}
})
@bp.post('/products')
@require_auth
@require_role('admin')
def create_product():
data = request.get_json() or {}
errors = ProductSchema().validate(data)
if errors:
return jsonify({'errors': errors}), 422
product = Product(
name=data['name'],
price=data['price'],
category_id=data.get('category_id')
)
db.session.add(product)
db.session.commit()
return jsonify(product.to_dict()), 201
Модели SQLAlchemy
from .extensions import db
from datetime import datetime
from slugify import slugify
class Product(db.Model):
__tablename__ = 'products'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
slug = db.Column(db.String(255), unique=True, nullable=False)
price = db.Column(db.Numeric(10, 2), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
attributes = db.Column(db.JSON, default=dict)
is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
category = db.relationship('Category', back_populates='products')
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.slug:
self.slug = slugify(self.name)
def to_dict(self) -> dict:
return {
'id': self.id,
'name': self.name,
'slug': self.slug,
'price': float(self.price),
'category': self.category.name if self.category else None
}
Валидация через Marshmallow
from marshmallow import Schema, fields, validate, validates, ValidationError
class ProductSchema(Schema):
name = fields.Str(required=True, validate=validate.Length(min=2, max=255))
price = fields.Float(required=True, validate=validate.Range(min=0.01))
category_id = fields.Int(load_default=None)
description = fields.Str(load_default=None)
@validates('category_id')
def validate_category(self, value):
if value is not None:
from ..models import Category
if not Category.query.get(value):
raise ValidationError('Категория не найдена')
JWT аутентификация
flask-jwt-extended — стандарт:
from flask_jwt_extended import (
create_access_token, create_refresh_token,
jwt_required, get_jwt_identity, get_jwt
)
@auth_bp.post('/login')
def login():
data = request.get_json()
user = User.query.filter_by(email=data.get('email')).first()
if not user or not user.check_password(data.get('password')):
return jsonify({'error': 'Invalid credentials'}), 401
additional_claims = {'role': user.role}
access_token = create_access_token(identity=user.id, additional_claims=additional_claims)
refresh_token = create_refresh_token(identity=user.id)
return jsonify({
'access_token': access_token,
'refresh_token': refresh_token
})
@auth_bp.post('/refresh')
@jwt_required(refresh=True)
def refresh():
user_id = get_jwt_identity()
access_token = create_access_token(identity=user_id)
return jsonify({'access_token': access_token})
# Декоратор для защиты маршрутов
def require_role(role: str):
def decorator(fn):
@wraps(fn)
@jwt_required()
def wrapper(*args, **kwargs):
claims = get_jwt()
if claims.get('role') != role:
return jsonify({'error': 'Forbidden'}), 403
return fn(*args, **kwargs)
return wrapper
return decorator
Загрузка файлов
import boto3
from werkzeug.utils import secure_filename
from PIL import Image
import io
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp'}
s3 = boto3.client('s3')
@bp.post('/upload')
@require_auth
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file provided'}), 400
file = request.files['file']
ext = file.filename.rsplit('.', 1)[-1].lower()
if ext not in ALLOWED_EXTENSIONS:
return jsonify({'error': 'File type not allowed'}), 400
img = Image.open(file.stream)
img.thumbnail((1920, 1080), Image.LANCZOS)
buffer = io.BytesIO()
img.save(buffer, format=img.format or 'JPEG', quality=85)
buffer.seek(0)
filename = f"uploads/{datetime.utcnow().strftime('%Y/%m')}/{secure_filename(file.filename)}"
s3.upload_fileobj(buffer, current_app.config['S3_BUCKET'], filename,
ExtraArgs={'ContentType': file.content_type})
return jsonify({'url': f"https://{current_app.config['CDN_HOST']}/{filename}"})
Обработка ошибок
@app.errorhandler(404)
def not_found(e):
return jsonify({'error': 'Not found'}), 404
@app.errorhandler(422)
def unprocessable(e):
return jsonify({'error': 'Unprocessable entity'}), 422
@app.errorhandler(Exception)
def handle_exception(e):
if isinstance(e, HTTPException):
return jsonify({'error': e.description}), e.code
# Логируем и возвращаем 500
current_app.logger.exception(e)
return jsonify({'error': 'Internal server error'}), 500
Развёртывание
Flask запускается через Gunicorn:
gunicorn "app:create_app('production')" \
--workers 4 \
--worker-class gevent \
--bind 0.0.0.0:5000 \
--timeout 30
Для async-операций — gevent worker или переход на Flask 3.x с async views.
Сроки разработки
- Scaffold + конфигурация + БД — 2–4 дня
- Модели + миграции — 3–5 дней
- API endpoints + auth — 1–2 недели
- Тесты (pytest + flask test client) — 3–5 дней
- Интеграции — по задаче
Небольшой или средний API для сайта: 3–7 недель. Flask проигрывает FastAPI в автодокументации и Django в набортных инструментах, но выигрывает в простоте и предсказуемости для проектов, где не нужно ни того, ни другого.







