Tu veux apprendre à coder une application web utile et concrète, pas juste un tuto théorique ?
Dans ce guide, tu vas créer une appli de gestion de bibliothèque avec Flask, MySQL et Bootstrap : fiches de livres, filtres, statistiques et dashboard responsive.
Pourquoi ce projet est parfait pour apprendre
Concret
Tu vas développer une application autour des livres, un sujet simple et accessible à tous. Pas besoin de connaissances techniques pointues pour comprendre la finalité : tu construis un outil clair, utile et motivant.
Complet
Ce projet couvre toutes les briques essentielles d’une application web moderne : gestion de contenu (CRUD), filtres dynamiques, affichage de statistiques, et création d’un mini-dashboard interactif. C’est un véritable terrain d’entraînement pour monter en compétence.
Moderne
Tu utiliseras des technologies actuelles et professionnelles comme Flask 3+, PyMySQL, Bootstrap 5.3 et Chart.js 4, te préparant ainsi aux standards du développement web Python.
Réutilisable
La structure du projet est suffisamment générique pour être adaptée à d’autres thématiques : gestion de films, de recettes, de jeux vidéo ou tout autre contenu basé sur des fiches.
Progressif
Le tutoriel est pensé étape par étape, avec une montée en complexité douce. Il est parfaitement adapté pour un rapport de stage, un projet étudiant, ou pour étoffer ton portfolio avec une application vraiment fonctionnelle.
Prérequis & installation
Avant de commencer, assure-toi de disposer des outils suivants pour suivre ce tutoriel dans de bonnes conditions :
Python 3.10 ou plus
Installe la dernière version de Python depuis le site officiel si ce n’est pas déjà fait. C’est la base de ton environnement de développement.
MySQL ou MariaDB
Tu auras besoin d’un serveur de base de données relationnelle pour stocker les livres. Une installation locale suffit, mais tu peux aussi utiliser une instance distante.
Visual Studio Code (VS Code)
C’est l’éditeur recommandé pour ce projet, car il offre une excellente intégration avec Python, Git, les environnements virtuels, et les extensions utiles au quotidien.
Mise en place de l’environnement
Une fois les outils installés, voici comment configurer ton projet proprement :
1. Crée un environnement virtuel
Cela permet d’isoler tes dépendances Python et d’éviter les conflits entre projets.
python -m venv venv
2. Active l’environnement virtuel
- Sous Windows :
.\venv\Scripts\activate
- Sous macOS/Linux :
source venv/bin/activate
3. Installe les dépendances nécessaires
Grâce au fichier requirements.txt
, toutes les bibliothèques utiles sont installées en une commande :
pip install -r requirements.txt
Structure complète du projet
Avant de coder, voici un aperçu de l’arborescence de ton projet Flask. Chaque dossier et fichier a un rôle clair dans l’architecture MVC :
bibliotheque/
├── app.py # Initialise Flask et PyMySQL
├── run.py # Lance l’appli en mode debug
├── config.py # Paramètres & secrets centralisés
├── .env # Variables sensibles (non commitées)
├── requirements.txt # Dépendances Python
├── db/
│ └── init_db.sql # Script SQL de création + données de test
├── controllers/
│ └── book_controller.py # Routes & logique métier
├── models/
│ └── book_model.py # Requêtes SQL vers la base
├── utils/
│ └── helpers.py # Fonctions utilitaires (ex : dictfetchall)
├── templates/ # HTML + Jinja2 (structure des vues)
│ ├── layout.html
│ ├── index.html
│ ├── add_book.html
│ ├── edit_book.html
│ └── stats.html
└── static/ # Fichiers statiques (CSS, JS, images)
├── css/style.css
└── js/chart.js
Pourquoi cette arborescence ?
Élément | Rôle | Pourquoi c’est important |
---|---|---|
app.py | Initialise Flask, connecte MySQL, enregistre les routes | Point central de l’application |
config.py | Stocke les paramètres de connexion et les clés secrètes | Tout est centralisé, facile à modifier |
models/ | Contient les classes qui interagissent avec la base | Respect du MVC : séparation claire |
controllers/ | Logique métier et routes HTTP | Lisibilité, modularité, testabilité |
templates/ | Gère les vues HTML via Jinja2 | Structure propre et réutilisable |
static/ | CSS, JS, images | Meilleures performances et organisation |
db/ | Script SQL pour la base | Facilite l’installation ou la réinitialisation du projet |
.env | Stocke les infos sensibles | Ne doit jamais être versionné (.gitignore ) |
Configuration & secrets
Pour que ton application fonctionne correctement et reste sécurisée, il est essentiel de séparer les informations sensibles (comme les identifiants MySQL ou la clé secrète de Flask) du code principal. C’est là qu’interviennent les fichiers config.py
et .env
.
config.py
– Centralise les paramètres et charge .env
import os
from dotenv import load_dotenv
load_dotenv() # Charge automatiquement le fichier .env
class Config:
SECRET_KEY = os.getenv("SECRET_KEY", "devkey")
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
MYSQL_USER = os.getenv("MYSQL_USER", "root")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
MYSQL_DB = os.getenv("MYSQL_DB", "bibliotheque")
MYSQL_CURSORCLASS = "DictCursor" # Retourne les résultats en dictionnaires
L’avantage est que tu peux modifier les paramètres de connexion ou de sécurité sans toucher au code, simplement en changeant les valeurs dans .env
.
.env
– Ne jamais versionner ce fichier
Ajoute-le à ton .gitignore
pour éviter de publier tes mots de passe ou clés secrètes. Le fichier .env
contient toutes les informations que tu ne veux surtout pas publier : mots de passe, clés secrètes, identifiants, etc. Il n’est pas obligatoire de créer un environnement virtuel, mais fortement recommandé.
SECRET_KEY=une_vraie_clef_ultra_secrete
MYSQL_USER=root
MYSQL_PASSWORD=monMDP
MYSQL_DB=bibliotheque
Point d’entrée Flask
Ton application Flask est découpée en deux fichiers : app.py
pour la configuration globale, et run.py
pour le lancement en développement.
app.py
— Le cœur de ton application Flask
Le fichier app.py
sert à construire et configurer ton application Flask. C’est là que tout est rassemblé :
- L’instanciation de l’objet Flask
- Le chargement des paramètres depuis
config.py
- L’initialisation des extensions (comme
MySQL
) - L’injection de dépendances (comme le modèle dans le contrôleur)
- L’enregistrement des blueprints (routes organisées)
# app.py
from flask import Flask
from flask_mysqldb import MySQL
from config import Config
from models.book_model import BookModel
from controllers.book_controller import book_bp
mysql = MySQL() # instance unique
def create_app() -> Flask:
app = Flask(__name__)
app.config.from_object(Config)
mysql.init_app(app) # Connexion MySQL via flask_mysqldb
# Injection du modèle dans le contrôleur
model = BookModel(mysql)
import controllers.book_controller as book_controller
book_controller.model = model
app.register_blueprint(book_bp)
return app
app = create_app()
app.py
ne lance pas l’application lui-même : il la prépare. Cela permet de réutiliser l’application ailleurs, par exemple en production avec un serveur comme Gunicorn ou uWSGI.
run.py
— Lancement local en mode développement
Ce fichier est très court, mais essentiel pendant le développement. Il importe l’application Flask construite dans app.py
et la lance en mode debug.
# run.py
from app import app
if __name__ == "__main__":
app.run(debug=True) # Auto‑reload et affichage des erreurs en clair
Ce que fait run.py
:
- Démarre le serveur de développement intégré de Flask
- Active le rechargement automatique à chaque modification de code
- Affiche les messages d’erreur détaillés en cas de plantage
🧪 À utiliser uniquement en local.
En production, on utilisera un serveur WSGI comme Gunicorn qui se base directement surapp.py
.
Le modèle : book_model.py
Le fichier book_model.py
contient toute la logique d’accès à la base de données. Il correspond à la lettre M dans l’architecture MVC (Modèle – Vue – Contrôleur). Son objectif est de centraliser les requêtes SQL, d’interagir avec la base via un curseur MySQL, et de retourner les données sous forme de dictionnaires pour les routes Flask.
from utils.helpers import dictfetchall
from datetime import date, timedelta
class BookModel:
def __init__(self, mysql):
self.db = mysql
# --- CRUD --------------------------------------------------
def get_all_books(self):
cur = self.db.connection.cursor()
cur.execute("SELECT * FROM books ORDER BY id DESC")
res = dictfetchall(cur)
cur.close()
return res
def get_filtered_books(self, status=None, author=None):
sql, params = "SELECT * FROM books WHERE 1=1", []
if status:
sql += " AND status=%s"; params.append(status)
if author:
sql += " AND author LIKE %s"; params.append(f"%{author}%")
sql += " ORDER BY id DESC"
cur = self.db.connection.cursor()
cur.execute(sql, params)
res = dictfetchall(cur)
cur.close()
return res
def insert_book(self, data):
cur = self.db.connection.cursor()
cur.execute("""
INSERT INTO books (title, author, summary, read_date, note, status)
VALUES (%s,%s,%s,%s,%s,%s)
""", (
data['title'],
data['author'],
data['summary'],
data['read_date'] or None,
data['note'] or None,
data['status']
))
self.db.connection.commit()
cur.close()
# ... méthodes update_book, delete_book à l’identique ...
# --- Statistiques ------------------------------------------
def stats_summary(self):
cur = self.db.connection.cursor()
cur.execute("""
SELECT COUNT(*) total,
SUM(status='à lire') to_read,
SUM(status='en cours') reading,
SUM(status='lu') finished,
ROUND(AVG(note),2) avg_note
FROM books
""")
row = cur.fetchone()
cur.close()
return row
def stats_status(self):
cur = self.db.connection.cursor()
cur.execute("SELECT status, COUNT(*) nb FROM books GROUP BY status")
rows = dictfetchall(cur)
cur.close()
return {
"labels": [r['status'] for r in rows],
"values": [r['nb'] for r in rows]
}
def stats_monthly(self):
cur = self.db.connection.cursor()
cur.execute("""
SELECT DATE_FORMAT(read_date,'%Y-%m') ym, COUNT(*) nb
FROM books
WHERE status='lu' AND read_date IS NOT NULL
AND read_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY ym ORDER BY ym
""")
rows = dictfetchall(cur)
cur.close()
labels, values = [], []
today = date.today().replace(day=1)
for i in range(11, -1, -1):
label = (today - timedelta(days=i * 30)).strftime("%Y-%m")
labels.append(label)
values.append(next((r['nb'] for r in rows if r['ym'] == label), 0))
return {"labels": labels, "values": values}
Importance : Centraliser les requêtes SQL dans ce fichier évite les duplications, rend le code testable et respecte le principe MVC.
Les routes : book_controller.py
Le contrôleur gère toutes les routes Flask : il récupère les données depuis le modèle, les transmet aux templates, et gère les actions de l’utilisateur.
from flask import Blueprint, render_template, request, redirect, url_for, flash
book_bp = Blueprint("book_bp", __name__)
model = None # Injecté depuis app.py
@book_bp.route("/")
def index():
status = request.args.get("status")
author = request.args.get("author")
books = model.get_filtered_books(status, author) if (status or author) else model.get_all_books()
return render_template("index.html", books=books)
@book_bp.route("/add", methods=["GET", "POST"])
def add_book():
if request.method == "POST":
model.insert_book(request.form)
flash("Livre ajouté ✅")
return redirect(url_for("book_bp.index"))
return render_template("add_book.html")
@book_bp.route("/edit/<int:book_id>", methods=["GET", "POST"])
def edit_book(book_id):
book = model.get_by_id(book_id)
if not book:
flash("Livre introuvable", "warning")
return redirect(url_for("book_bp.index"))
if request.method == "POST":
model.update_book(book_id, request.form)
flash("Mise à jour réussie 💾")
return redirect(url_for("book_bp.index"))
return render_template("edit_book.html", book=book)
@book_bp.route("/delete/<int:book_id>")
def delete_book(book_id):
model.delete_book(book_id)
flash("Livre supprimé 🗑️")
return redirect(url_for("book_bp.index"))
@book_bp.route("/stats")
def stats():
return render_template("stats.html",
summary_cards=[
{"label": "Total", "value": model.stats_summary()["total"]},
{"label": "À lire", "value": model.stats_summary()["to_read"]},
{"label": "En cours", "value": model.stats_summary()["reading"]},
{"label": "Terminés", "value": model.stats_summary()["finished"]},
{"label": "Note moy.", "value": model.stats_summary()["avg_note"] or "–"},
],
stats_status=model.stats_status(),
stats_monthly=model.stats_monthly()
)
Ce fichier correspond à la lettre C de l’architecture MVC : le contrôleur.
Il fait le lien entre :
- les modèles (accès aux données),
- les vues (fichiers HTML),
- et les actions utilisateur (ajout, édition, suppression…).
À quoi sert le contrôleur ?
Il utilise flash()
pour afficher des messages de retour utilisateur
Il gère les routes HTTP accessibles via le navigateur (comme /
, /add
, /edit/5
, etc.)
Il interagit avec le modèle pour récupérer ou modifier des données
Il transmet les données aux templates HTML
Helpers utilitaires
Ton fichier utils/helpers.py
contient une simple fonction pour récupérer les résultats SQL sous forme de dictionnaires. Un petit outil très pratique qui t’évitera de répéter ce code partout.
# utils/helpers.py
def dictfetchall(cursor):
"""Transforme le résultat en liste de dictionnaires."""
return [row for row in cursor.fetchall()]
Cette fonction dictfetchall
transforme le résultat d’un curseur SQL en une liste de dictionnaires, ce qui rend les données bien plus faciles à manipuler dans Flask, notamment pour les afficher dans les templates HTML.
C’est un outil simple mais indispensable dans une application qui interagit souvent avec une base de données.
Templates HTML (Bootstrap 5)
Ton design repose sur un template principal layout.html
que tous les autres fichiers héritent. Il gère la navigation, le style sombre et les blocs de contenu.
<!-- templates/layout.html -->
<!DOCTYPE html>
<html lang="fr" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{% block title %}Ma Bibliothèque{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
{% block extra_head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand ms-3" href="{{ url_for('book_bp.index') }}">📚 Bibliothèque</a>
<button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto me-3">
<li class="nav-item">
<a class="nav-link {% if request.endpoint=='book_bp.index' %}active{% endif %}" href="{{ url_for('book_bp.index') }}">Livres</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint=='book_bp.stats' %}active{% endif %}" href="{{ url_for('book_bp.stats') }}">Statistiques</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('book_bp.add_book') }}">➕ Ajouter</a>
</li>
</ul>
</div>
</nav>
<main class="container py-4">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for m in messages %}
<div class="alert alert-info">{{ m }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
CSS & Chart.js
Tu peux personnaliser rapidement l’apparence et les graphes avec deux petits fichiers statiques.
static/css/style.css
(extrait)
:root {
--accent: #6366f1;
}
body {
background: #111827;
color: #f1f5f9;
}
.btn-accent {
background: var(--accent);
border: none;
}
.btn-accent:hover {
background: #4f46e5;
}
.card-dark {
background: #1f2937;
border: 1px solid #334155;
border-radius: 1rem;
}
static/js/chart.js
document.addEventListener("DOMContentLoaded", () => {
const parseJSON = id => JSON.parse(document.getElementById(id).textContent);
// Doughnut chart : répartition par statut
const st = parseJSON("jsonStatus");
new Chart(document.getElementById("chartStatus"), {
type: "doughnut",
data: { labels: st.labels, datasets: [{ data: st.values }] },
options: { plugins: { legend: { position: "bottom" } } }
});
// Line chart : progression mensuelle
const mo = parseJSON("jsonMonthly");
new Chart(document.getElementById("chartMonthly"), {
type: "line",
data: { labels: mo.labels, datasets: [{ data: mo.values, tension: .3, fill: true }] },
options: {
scales: { y: { beginAtZero: true } },
plugins: { legend: { display: false } }
}
});
});
Le CSS et le JavaScript jouent un rôle essentiel dans l’expérience utilisateur de l’application.
Le CSS — Pour un design cohérent et moderne
Le fichier style.css
permet de personnaliser l’apparence de ton application au-delà du style par défaut de Bootstrap. Il apporte une identité visuelle à ton interface : couleurs, boutons personnalisés, cartes sombres, etc.
Un design soigné rend l’application plus agréable à utiliser et plus crédible dans un contexte pro (stage, démonstration, portfolio…).
JavaScript — Pour l’interactivité et la visualisation
Le fichier chart.js
utilise Chart.js 4 pour générer dynamiquement des graphiques interactifs dans la page Statistiques. Il transforme des données brutes en visuels clairs, digestes et responsives.
Grâce à JavaScript, l’utilisateur peut visualiser l’évolution de ses lectures, la répartition des statuts, ou encore la moyenne des notes, sans recharger la page.
Script SQL de base
Ton fichier db/init_db.sql
crée la base, la table books
et insère 20 enregistrements réalistes pour tes tests.
-- Création de la base
CREATE DATABASE IF NOT EXISTS bibliotheque CHARACTER SET utf8mb4;
USE bibliotheque;
DROP TABLE IF EXISTS books;
CREATE TABLE books (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author VARCHAR(255),
summary TEXT,
read_date DATE,
note TINYINT CHECK (note BETWEEN 0 AND 10),
status ENUM('à lire','en cours','lu') DEFAULT 'à lire'
);
Le fichier init_db.sql
, situé dans le dossier db/
, contient toutes les instructions nécessaires pour créer la base de données, définir la table principale et ajouter des données de test.
Il te permet d’initialiser rapidement ton environnement de développement ou de réinstaller la base à l’identique en une seule commande.
Tu peux te rendre sur ton serveur si tu l’as installé en local, ici j’utilises Wampserver, et créer ta bdd soit graphiquement ou à partir du script

Pour exécuter le script si tu veux le lancer à partir du ficher init.sql:
mysql -u root -p < db/init_db.sql
Lancer l’application
Une fois tous les fichiers en place, la base de données créée, et les dépendances installées, il ne te reste plus qu’à démarrer ton application Flask pour la tester en local. Tu es prêt à voir ton projet en action ? Rien de plus simple :
python run.py
Ensuite, ouvre ton navigateur à l’adresse suivante :
http://127.0.0.1:5000
Teste les fonctionnalités :

- ➕ Ajoute un livre
- ✏️ Modifie-le
- 📊 Consulte les statistiques
🎯 Tu as entre les mains un vrai dashboard responsive !


Félicitations ! 🎓 Tu viens de créer une application web complète avec Flask, MySQL, Bootstrap et Chart.js, en suivant une approche claire, modulaire et moderne.
Tu maîtrises désormais :
- Le CRUD complet avec filtres dynamiques
- L’organisation MVC propre et scalable
- L’utilisation de templates Bootstrap avec Jinja2
- L’affichage de statistiques interactives via Chart.js
- La séparation entre configuration, logique métier et vues
Et maintenant ?
Tu peux télécharger le projet complet sur GitHub pour l’adapter, l’améliorer ou simplement l’étudier :
👉 Voir le dépôt GitHub
Des questions, suggestions ou idées d’amélioration ?
N’hésite pas à laisser un commentaire, je répondrai avec plaisir.
N’hésites pas aussi à consulte les autres articles du blog et à t’abonner à la NewsLetter pour ne rien rater. A bientôt !