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.







