Розробка бекенду сайту на Python (Flask)
Flask залишається актуальним не тому що застарів, а тому що правильно розставляє відповідальності. Це мікрофреймворк: він дає HTTP-маршрутизацію, request/response контекст та ніче зайвого. ORM, серіалізацію, аутентифікацію, кешування — вибираєте та збираєте самі. Це слабість для новачків та сила для досвідчених команд, які хочуть контролю.
Flask добре підходить для: прототипів, невеликих API, сервісів з нестандартною логікою, проектів де Django надлишковий, а FastAPI — overengineering.
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 в вбудованих інструментах, але виграє в простоті та передбачуваності для проектів, де не потрібне ні те, ні те.







