Website Backend Development with Python (Flask)

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Website Backend Development with Python (Flask)
Medium
~3-5 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Website Backend Development with Python (Flask)

Flask remains relevant not because it's outdated, but because it correctly assigns responsibilities. It's a microframework: it provides HTTP routing, request/response context and nothing extra. ORM, serialization, authentication, caching — you choose and assemble them yourself. This is a weakness for beginners and a strength for experienced teams that want control.

Flask works well for: prototypes, small APIs, services with non-standard logic, projects where Django is overkill and FastAPI is overengineering.

Application Factory and Blueprints

Proper Flask initialization is through a factory. This allows creating multiple instances with different configurations (especially important for tests):

# 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 registration
    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 isolates a group of routes:

# 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 Models

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
        }

Validation via 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('Category not found')

JWT Authentication

flask-jwt-extended is the standard:

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})

# Decorator for route protection
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

File Upload

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}"})

Error Handling

@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
    # Log and return 500
    current_app.logger.exception(e)
    return jsonify({'error': 'Internal server error'}), 500

Deployment

Flask runs through Gunicorn:

gunicorn "app:create_app('production')" \
  --workers 4 \
  --worker-class gevent \
  --bind 0.0.0.0:5000 \
  --timeout 30

For async operations — gevent worker or upgrade to Flask 3.x with async views.

Development Timelines

  • Scaffold + configuration + DB — 2–4 days
  • Models + migrations — 3–5 days
  • API endpoints + auth — 1–2 weeks
  • Tests (pytest + flask test client) — 3–5 days
  • Integrations — depends on task

Small to medium API for a website: 3–7 weeks. Flask loses to FastAPI in auto-documentation and Django in built-in tools, but wins in simplicity and predictability for projects that need neither.