Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

Flask 3

🎯 Objectif : utiliser Flask 3 pour des projets ciblés (microservices, scripts web, prototypes) en assemblant les briques dont tu as besoin.

À l'issue de cet axe, tu sauras :

  • Construire une app Flask avec routes, request, response
  • Organiser le code avec Blueprints et l'application factory pattern
  • Persister avec SQLAlchemy 2 et Flask-SQLAlchemy 3
  • Sérialiser avec Marshmallow
  • Authentifier avec Flask-JWT-Extended
  • Tester avec le client de test natif Flask
PourContre
Minimaliste : tu choisis tout (ORM, validation, auth)Pas de magie — tout à assembler
Excellent pour comprendre ce qu’un framework fait vraimentPas de doc API auto comme FastAPI
Stable depuis 15+ ansAsync limité (utiliser Quart pour vraiment async)
Idéal pour microservices ciblés ou prototypesÀ grande échelle, on assemble beaucoup d’extensions

Verdict 2026 : Flask reste pertinent pour des projets bien définis (un endpoint webhook, un CLI HTTP, un mini-service). Pour un nouveau projet ambitieux, FastAPI est plus adapté.

Fenêtre de terminal
uv init taskly-flask
cd taskly-flask
uv add flask flask-sqlalchemy flask-jwt-extended marshmallow
# app.py
app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/health")
def health():
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(debug=True)
5000/health
uv run python app.py

Pour les projets > 100 lignes, structure en factory plutôt qu’un seul fichier :

app/__init__.py
from flask import Flask
from .extensions import db, jwt
from .config import Config
def create_app(config_class: type[Config] = Config) -> Flask:
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
jwt.init_app(app)
# Enregistrer les blueprints
from .modules.auth import bp as auth_bp
from .modules.tasks import bp as tasks_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(tasks_bp, url_prefix='/tasks')
return app
app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
db = SQLAlchemy()
jwt = JWTManager()
app/config.py
import os
class Config:
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///./data.db")
JWT_SECRET_KEY = os.getenv("JWT_SECRET")
JWT_TOKEN_LOCATION = ["cookies"]
JWT_COOKIE_SECURE = os.getenv("FLASK_ENV") == "production"
JWT_COOKIE_SAMESITE = "Lax"
wsgi.py
from app import create_app
app = create_app()
# Lancement
# uv run flask --app wsgi run --debug
# Prod : uv run gunicorn wsgi:app

Un blueprint = un sous-ensemble de routes regroupées. Permet d’organiser et d’éviter les fichiers monstrueux.

app/modules/tasks/__init__.py
from flask import Blueprint
bp = Blueprint('tasks', __name__)
from . import routes # noqa (charge les routes)
app/modules/tasks/routes.py
from flask import jsonify, request
from flask_jwt_extended import jwt_required, current_user
from . import bp
from .schemas import task_schema, tasks_schema, create_task_schema
from app.extensions import db
from app.models import Task
@bp.get('/')
@jwt_required()
def list_tasks():
tasks = Task.query.filter_by(owner_id=current_user.id).all()
return jsonify(tasks_schema.dump(tasks))
@bp.post('/')
@jwt_required()
def create_task():
payload = create_task_schema.load(request.json or {})
task = Task(owner_id=current_user.id, **payload)
db.session.add(task)
db.session.commit()
return jsonify(task_schema.dump(task)), 201
app/models.py
from datetime import datetime
from app.extensions import db
class User(db.Model):
id: int = db.Column(db.Integer, primary_key=True)
email: str = db.Column(db.String(255), unique=True, nullable=False)
name: str = db.Column(db.String(100), nullable=False)
password_hash: str = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Task(db.Model):
id: int = db.Column(db.Integer, primary_key=True)
owner_id: int = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title: str = db.Column(db.String(200), nullable=False)
description: str | None = db.Column(db.Text)
done: bool = db.Column(db.Boolean, default=False, nullable=False)
due_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
owner = db.relationship('User', backref='tasks')

Marshmallow est le pendant Flask de Pydantic (FastAPI) ou Serializers (DRF).

app/modules/tasks/schemas.py
from marshmallow import Schema, fields, validate
class CreateTaskSchema(Schema):
title = fields.Str(required=True, validate=validate.Length(min=1, max=200))
description = fields.Str(validate=validate.Length(max=2000))
due_at = fields.DateTime()
class TaskSchema(Schema):
id = fields.Int()
title = fields.Str()
description = fields.Str()
done = fields.Bool()
due_at = fields.DateTime()
created_at = fields.DateTime()
create_task_schema = CreateTaskSchema()
task_schema = TaskSchema()
tasks_schema = TaskSchema(many=True)
app/modules/auth/routes.py
from flask import jsonify, request
from flask_jwt_extended import (
create_access_token, set_access_cookies, unset_jwt_cookies, jwt_required
)
from werkzeug.security import check_password_hash, generate_password_hash
from . import bp
from app.extensions import db
from app.models import User
@bp.post('/register')
def register():
data = request.json or {}
if User.query.filter_by(email=data['email']).first():
return jsonify({'error': 'Email already used'}), 409
user = User(
email=data['email'],
name=data['name'],
password_hash=generate_password_hash(data['password']),
)
db.session.add(user)
db.session.commit()
return jsonify({'id': user.id, 'email': user.email, 'name': user.name}), 201
@bp.post('/login')
def login():
data = request.json or {}
user = User.query.filter_by(email=data['email']).first()
if not user or not check_password_hash(user.password_hash, data['password']):
return jsonify({'error': 'Invalid credentials'}), 401
token = create_access_token(identity=str(user.id))
response = jsonify({'id': user.id, 'email': user.email, 'name': user.name})
set_access_cookies(response, token)
return response
@bp.post('/logout')
@jwt_required()
def logout():
response = jsonify(success=True)
unset_jwt_cookies(response)
return response
tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
@pytest.fixture
def app():
app = create_app()
app.config.update(TESTING=True, SQLALCHEMY_DATABASE_URI="sqlite:///:memory:")
with app.app_context():
db.create_all()
yield app
@pytest.fixture
def client(app):
return app.test_client()
# tests/test_auth.py
def test_register(client):
response = client.post('/auth/register', json={
'email': 'alice@example.com',
'name': 'Alice',
'password': 'password123',
})
assert response.status_code == 201
assert response.json['email'] == 'alice@example.com'

Si tu as besoin de vraies perfs async tout en gardant l’API Flask, Quart est un drop-in replacement async. Mais en 2026, FastAPI est généralement le meilleur choix pour ça.

FROM python:3.13-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen
COPY . .
CMD ["uv", "run", "gunicorn", "wsgi:app", "--workers", "4", "--bind", "0.0.0.0:8000"]
Tu démarres un microservice qui reçoit des webhooks GitHub et les forward vers Slack. Choix idéal en Python ?
Tu organises ton app Flask en plusieurs modules. Quel pattern utilises-tu ?
Tu fais `result = User.query.filter_by(email=user_input).first()`. Est-ce sûr ?

Suite : FastAPI — pour le projet taskly-api.