Розробка бекенду сайту на Python (Flask)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка бекенду сайту на Python (Flask)
Середня
~3-5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка бекенду сайту на 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 в вбудованих інструментах, але виграє в простоті та передбачуваності для проектів, де не потрібне ні те, ні те.