1. Installation & Hello World

pip install flask python-dotenv
# Vérifier
python -c "import flask; print(flask.__version__)"
# ✅ app.py — structure minimale
from flask import Flask

app = Flask(__name__)   # __name__ = nom du module courant

@app.get("/")
def index():
    return {"message": "Bonjour Flask !", "version": "1.0"}

if __name__ == "__main__":
    # ❌ debug=True uniquement en développement
    app.run(debug=True, port=5000)
# Lancer avec la variable d'environnement (recommandé)
export FLASK_APP=app.py
export FLASK_DEBUG=1
flask run

# Ou directement
python app.py

2. Routes & méthodes HTTP

from flask import Flask, request, jsonify

app = Flask(__name__)

# ── Méthodes HTTP explicites (Flask 2.0+)
@app.get("/api/tasks")
def list_tasks():
    return jsonify({"tasks": []})

@app.post("/api/tasks")
def create_task():
    return jsonify({"message": "créé"}), 201

@app.put("/api/tasks/<int:task_id>")
def update_task(task_id: int):
    return jsonify({"id": task_id, "updated": True})

@app.delete("/api/tasks/<int:task_id>")
def delete_task(task_id: int):
    return "", 204   # No Content

# ── Paramètres de chemin typés
@app.get("/api/tasks/<int:task_id>")   # <int:x> → int
def get_task(task_id: int):
    return jsonify({"id": task_id})

@app.get("/api/users/<string:username>")  # <string:x> → str
def get_user(username: str):
    return jsonify({"username": username})

# ── Paramètres de query string (?page=1&limit=10)
@app.get("/api/search")
def search():
    q     = request.args.get("q", "")
    page  = request.args.get("page", 1, type=int)
    limit = request.args.get("limit", 10, type=int)
    return jsonify({"q": q, "page": page, "limit": limit})

3. Request & Response

from flask import request, jsonify, make_response

# ── Lire le body JSON
@app.post("/api/tasks")
def create_task():
    # ✅ get_json() retourne None si Content-Type incorrect
    data = request.get_json(silent=True)
    if not data:
        return jsonify({"error": "Body JSON requis"}), 400

    titre = data.get("titre", "").strip()
    if not titre:
        return jsonify({"error": "Le titre est obligatoire"}), 400

    # Créer la tâche...
    nouvelle = {"id": 1, "titre": titre, "statut": "todo"}
    return jsonify(nouvelle), 201

# ── Lire les headers
@app.get("/api/me")
def me():
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        return jsonify({"error": "Token manquant"}), 401
    token = auth[7:]
    return jsonify({"token": token[:8] + "..."})

# ── Réponse avec headers personnalisés
@app.get("/api/health")
def health():
    resp = make_response(jsonify({"status": "ok"}), 200)
    resp.headers["X-API-Version"] = "1.0"
    return resp

# ── Codes HTTP courants
# 200 OK           → succès lecture
# 201 Created      → ressource créée
# 204 No Content   → succès sans body (DELETE)
# 400 Bad Request  → données invalides
# 401 Unauthorized → non authentifié
# 403 Forbidden    → authentifié mais pas autorisé
# 404 Not Found    → ressource inexistante
# 409 Conflict     → conflit (ex : email déjà utilisé)
# 500 Server Error → erreur serveur

4. Blueprint — Découpage en modules

# ── tasks/routes.py
from flask import Blueprint, jsonify, request

tasks_bp = Blueprint("tasks", __name__, url_prefix="/api/tasks")

# Stockage en mémoire (demo)
_tasks: list[dict] = []
_next_id = 1

@tasks_bp.get("/")
def list_tasks():
    statut = request.args.get("statut")
    result = [t for t in _tasks if not statut or t["statut"] == statut]
    return jsonify(result)

@tasks_bp.post("/")
def create_task():
    global _next_id
    data = request.get_json(silent=True) or {}
    t = {"id": _next_id, "titre": data.get("titre", ""), "statut": "todo"}
    _tasks.append(t)
    _next_id += 1
    return jsonify(t), 201

# ── app.py — enregistrement du Blueprint
from flask import Flask
from tasks.routes import tasks_bp

app = Flask(__name__)
app.register_blueprint(tasks_bp)   # préfixe /api/tasks défini dans le Blueprint
Bonne pratique : Un Blueprint par ressource (tasks, users, projects). Chaque Blueprint dans son propre fichier ou dossier.

5. Gestion d'erreurs

from flask import Flask, jsonify, abort

app = Flask(__name__)

# ── Gestionnaire d'erreur global JSON
@app.errorhandler(404)
def not_found(e):
    return jsonify({"error": "Ressource introuvable", "code": 404}), 404

@app.errorhandler(400)
def bad_request(e):
    return jsonify({"error": str(e.description), "code": 400}), 400

@app.errorhandler(500)
def server_error(e):
    return jsonify({"error": "Erreur interne du serveur", "code": 500}), 500

# ── abort() : lève une HTTPException
@app.get("/api/tasks/<int:task_id>")
def get_task(task_id: int):
    task = next((t for t in _tasks if t["id"] == task_id), None)
    if task is None:
        abort(404, description=f"Tâche #{task_id} introuvable")
    return jsonify(task)

# ── Exception custom dans Flask
class ValidationError(Exception):
    def __init__(self, message: str, field: str = None):
        super().__init__(message)
        self.field = field

@app.errorhandler(ValidationError)
def handle_validation(e):
    resp = {"error": str(e)}
    if e.field:
        resp["field"] = e.field
    return jsonify(resp), 422

6. Configuration & variables d'environnement

# ── config.py
import os
from dataclasses import dataclass

@dataclass
class Config:
    SECRET_KEY:   str  = os.getenv("SECRET_KEY", "dev-secret-change-me")
    DATABASE_URL: str  = os.getenv("DATABASE_URL", "sqlite:///app.db")
    DEBUG:        bool = os.getenv("FLASK_DEBUG", "0") == "1"
    TESTING:      bool = False

@dataclass
class ProductionConfig(Config):
    DEBUG: bool = False

CONFIGS = {
    "development": Config,
    "production":  ProductionConfig,
}

# ── app.py
from flask import Flask
from config import CONFIGS
import os

app = Flask(__name__)
env = os.getenv("FLASK_ENV", "development")
app.config.from_object(CONFIGS[env])
# .env (ne jamais committer)
SECRET_KEY=super-secret-production-key
DATABASE_URL=postgresql://user:pass@localhost/taskdb
FLASK_ENV=production

7. Context Flask — before_request & g

from flask import Flask, g, request
import time

app = Flask(__name__)

# ── before_request : s'exécute avant chaque requête
@app.before_request
def log_request():
    g.start_time = time.time()
    print(f"→ {request.method} {request.path}")

# ── after_request : modifier la réponse
@app.after_request
def add_timing_header(response):
    elapsed = time.time() - getattr(g, "start_time", time.time())
    response.headers["X-Process-Time"] = f"{elapsed:.4f}s"
    return response

# ── g : stockage par requête (réinitialisé à chaque requête)
@app.before_request
def load_current_user():
    token = request.headers.get("Authorization", "")[7:]
    if token:
        g.current_user = {"id": 1, "email": "alice@mail.fr"}
    else:
        g.current_user = None

@app.get("/api/profile")
def profile():
    if g.current_user is None:
        return {"error": "Non authentifié"}, 401
    return {"user": g.current_user}
← Module 01 Module 03 — FastAPI →