Relier les routes aux actions de la base de données : l'architecture
- Expliquer la séparation main.py (routes, HTTP) face à db.py (SQL) et pourquoi elle est importante
- Associer chaque endpoint à une action SQL et au code d’état correspondant
- Décrire ce qui se passe dans une requête — du navigateur, en passant par FastAPI et db.py, jusqu’à la base de données et retour
- Implémenter et exécuter GET /students et GET /students/{student_id} au-dessus de la base de données
Qu’allons-nous apprendre ?
Maintenant, nous relions tout : le serveur du thème backend et FastAPI, et la base de données du thème des bases de données. Cette leçon se concentre sur l’architecture, c’est-à-dire la façon dont les parties se relient, et implémente les deux premiers endpoints (la lecture). Dans la leçon suivante, le reste.
Pourquoi en avons-nous besoin ?
Dans le thème backend et FastAPI, le serveur gardait les étudiants en RAM, et ils disparaissaient à chaque redémarrage. Maintenant, le serveur enregistrera dans la base de données : des données qui restent, avec des contraintes et des transactions. C’est l’architecture de base de presque tous les backends du monde : client → API → base de données.
Explication à partir des bases
Deux fichiers, deux responsabilités
students-api/
├── main.py ← the API layer: routes, HTTP, validation, status codes
└── db.py ← the data layer: SQL, connections, commit (from the databases topic)
MySQL in Docker (kodkod-mysql) ← where the data livesmain.py | db.py | |
|---|---|---|
| Connaît | HTTP, les requêtes, les codes d’état, JSON, Pydantic | SQL, mysql.connector, les tables, commit |
| Ne connaît pas | SQL, les connexions | HTTP, 404, FastAPI |
| Le langage des erreurs | HTTPException(404) | None, False, ValueError |
C’est la « séparation de base du code entre la route et le code d’accès à la base de données » que demande le programme. Et pourquoi est-ce une bonne chose ?
- Chaque fichier est petit et ciblé (SRP, le thème du code propre).
- On teste chaque couche seule : nous avons déjà testé
db.pydans la leçon précédente, sans serveur. - On remplace une couche sans toucher à l’autre : passer à PostgreSQL ne changera que
db.py. Une autre interface (une application en ligne de commande) utilisera le mêmedb.py.
Le tableau de correspondance
| Endpoint | Fonction dans db.py | SQL | Succès | Échecs |
|---|---|---|---|---|
GET /students | get_all_students() | SELECT | 200 + une liste | — |
GET /students/{id} | get_student(id) | SELECT WHERE id | 200 + un dict | None → 404 |
POST /students | create_student(...) | INSERT | 201 + un dict | ValueError → 409. Validation → 400 / 422 |
PUT /students/{id} | update_student(id, ...) | UPDATE WHERE id | 200 + un dict | None → 404. ValueError → 409 |
DELETE /students/{id} | delete_student(id) | DELETE WHERE id | 204 | False → 404 |
Chaque route fait trois choses : (1) reçoit et valide l’entrée (FastAPI + validation), (2) appelle une fonction de db.py, (3) traduit le résultat en HTTP.
Une requête, de bout en bout
GET /students/2
│
▼ Uvicorn receives the HTTP request (port 8000)
▼ FastAPI finds the route, converts "2" → 2 (int), calls get_student(2)
▼ main.get_student → db.get_student(2)
▼ db: open connection → SELECT ... WHERE id = %s (2) → fetchone → dict → close
▼ main: dict is not None → return it
▼ FastAPI: dict → JSON, status 200
▼ Uvicorn sends the HTTP response
{"id": 2, "name": "Avi", "age": 31, "email": "avi@mail.com"}Tout le cours en un seul parcours : HTTP et le port (le thème sur l’ordinateur, le système d’exploitation et le réseau), les fonctions, les dicts et JSON (les thèmes Python), les classes et Pydantic (le thème POO), SQL (le thème des bases de données).
main.py = le serveur : il prend les commandes des convives, vérifie qu’elles ont du sens (« nous n’avons pas de pizza au menu »), les transmet à la cuisine, et sert. db.py = le magasinier de la cuisine : il sait où se trouve chaque ingrédient, il sort et range. Le serveur n’entre pas dans la réserve, et le magasinier ne parle pas aux convives. Chacun dans son rôle.
Quand la table est-elle créée ?
db.init_db() est appelée une seule fois, au démarrage du serveur (au niveau supérieur de main.py). Grâce à IF NOT EXISTS, c’est sans risque à chaque démarrage : la première fois la table est créée, et les fois suivantes rien ne se passe.
Syntaxe
from fastapi import FastAPI, HTTPException
import db # our data layer (same folder)
app = FastAPI(title="Students API")
db.init_db() # create the table once, on startup
@app.get("/students/{student_id}")
def get_student(student_id: int):
student = db.get_student(student_id) # 1 call to the data layer
if student is None: # translate "not found" to HTTP
raise HTTPException(status_code=404, detail="student not found")
return student # dict → JSON (200)Des noms identiques dans deux couches : la fonction de main.py s’appelle get_student, tout comme celle de db.py. Il n’y a pas de collision, car on appelle celle de la base par le nom du module : db.get_student(...) (souvenez-vous des modules !).
Premier exemple de code
Mettre en place le projet (le conteneur MySQL doit tourner — docker start kodkod-mysql) :
mkdir ~\dev\kodkod6\week-4\students-api
cd ~\dev\kodkod6\week-4\students-api
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install "fastapi[standard]" mysql-connector-python
git init.gitignore :
.venv/
__pycache__/
.envCopiez db.py du thème des bases de données (le CRUD complet) dans le dossier. Et main.py, à ce stade avec seulement deux endpoints :
from fastapi import FastAPI, HTTPException
import db
app = FastAPI(title="Students API")
db.init_db()
@app.get("/students")
def list_students():
return db.get_all_students()
@app.get("/students/{student_id}")
def get_student(student_id: int):
student = db.get_student(student_id)
if student is None:
raise HTTPException(status_code=404, detail="student not found")
return studentAjouter des données pour tester (la base est vide ! Il n’y a pas encore de POST). seed.py :
import db
db.init_db()
for name, age, email in [("Dana", 24, "dana@mail.com"), ("Avi", 31, "avi@mail.com")]:
try:
print(db.create_student(name, age, email))
except ValueError as error:
print("Skipped:", error)python seed.py
uvicorn main:app --reloadDans le navigateur :
http://127.0.0.1:8000/studentshttp://127.0.0.1:8000/students/2http://127.0.0.1:8000/students/99
Les réponses :
[{"id":1,"name":"Dana","age":24,"email":"dana@mail.com"},{"id":2,"name":"Avi","age":31,"email":"avi@mail.com"}]{"id":2,"name":"Avi","age":31,"email":"avi@mail.com"}{"detail":"student not found"}Analyse du code ligne par ligne
| Code | Explication |
|---|---|
import db | L’import de la couche de données. main.py et db.py sont dans le même dossier (la leçon sur les modules). |
db.init_db() | S’exécute une fois quand Uvicorn charge main.py : la table est prête avant la première requête. |
def list_students(): return db.get_all_students() | Une route en une ligne. La liste (de dicts) devient du JSON. |
student_id: int | FastAPI convertit et vérifie (/students/abc → 422, le thème FastAPI). |
db.get_student(student_id) | Un seul appel à la couche de données. |
if student is None: raise HTTPException(404) | La traduction : le « introuvable » de db.py → le 404 de HTTP. |
seed.py | Seed = des données de départ. Un script séparé, qui utilise le même db.py. La preuve que la couche ne dépend pas du serveur. |
Et maintenant l’expérience importante : arrêtez le serveur (Ctrl + C), et relancez-le. Actualisez /students. Les données sont là ! Contrairement au thème FastAPI, elles sont dans la base de données — dans le serveur MySQL du conteneur, qui les écrit sur le stockage (le thème sur l’ordinateur !). Vous pouvez aussi les voir dans Database Client.
Un autre exemple
Un autre endpoint, qui utilise db.py avec un Query Parameter : la recherche. Ajoutez à db.py le search_students de la pratique de la leçon précédente, et à main.py :
@app.get("/students/search")
def search_students(name: str):
if len(name.strip()) < 2:
raise HTTPException(status_code=400, detail="search text must have at least 2 characters")
return db.search_students(name)Où dans le fichier ? /students/search est un chemin fixe, il doit donc venir avant /students/{student_id} (le thème FastAPI, l’erreur 3 !). Sinon, search sera attrapé comme un student_id → 422.
GET /students/search?name=av → 200 [{"id": 2, "name": "Avi", ...}]
GET /students/search?name=a → 400 {"detail": "search text must have at least 2 characters"}
GET /students/search → 422 (name is required)Remarquez la répartition du travail : la validation (au moins 2 caractères) est dans main.py, car c’est une règle de l’API. Le SQL (LIKE, lower) est dans db.py.
Erreur courante
Erreur 1 : du SQL dans la route.
@app.get("/students/{student_id}")
def get_student(student_id: int):
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
...Cela « fonctionne », mais il y a maintenant du SQL à deux endroits, des informations de connexion à deux endroits, et une route qui fait tout. Route = entrée + un appel à db + traduction.
Erreur 2 : renvoyer None depuis la route.
@app.get("/students/{student_id}")
def get_student(student_id: int):
return db.get_student(student_id)Pour un étudiant qui n’existe pas, c’est 200 qui est renvoyé avec null. Le client pensera que cela a réussi. « Introuvable » = 404.
Erreur 3 : le serveur ne démarre pas — le conteneur MySQL ne tourne pas. db.init_db() s’exécute quand Uvicorn charge main.py, le serveur plante donc dès le démarrage :
mysql.connector.errors.InterfaceError: 2003: Can't connect to MySQL server on 'localhost:3306' ...Vérifiez avec docker ps, et démarrez-le : docker start kodkod-mysql (le thème des bases de données !).
Erreur 4 : ModuleNotFoundError: No module named 'db'. db.py n’est pas dans le même dossier que main.py, ou vous lancez uvicorn depuis un autre dossier. Lancez-le depuis le dossier du projet.
Exercices
Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.
Exercice 1: Deux couches
Pour chaque ligne de code — appartient-elle à main.py (la couche HTTP) ou à db.py (la couche de données) ?
cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))raise HTTPException(status_code=404, detail="student not found")@app.get("/students/{student_id}")mysql.connector.connect(host="localhost", user="root", ...)return db.get_student(student_id)
Afficher la solution
- db.py — du SQL.
- main.py — un code HTTP.
- main.py — une route.
- db.py — les informations de connexion à la base.
- main.py — le serveur appelle la couche de données.
La règle : main.py parle HTTP, db.py parle SQL. Aucun des deux ne connaît les détails de l’autre.
Exercice 2: Des routes de lecture
Vous avez le db.py de la leçon précédente (avec init_db, get_all_students et get_student). Écrivez un main.py avec :
db.init_db()au démarrage du serveur ;GET /students— tous les étudiants ;GET /students/{student_id}— un étudiant, ou 404 avec"student not found".
Afficher la solution
main.py
from fastapi import FastAPI, HTTPException
import db
app = FastAPI(title="Students API")
db.init_db()
@app.get("/students")
def list_students():
return db.get_all_students()
@app.get("/students/{student_id}")
def get_student(student_id: int):
student = db.get_student(student_id)
if student is None:
raise HTTPException(status_code=404, detail="student not found")
return studentdb.get_student renvoie None quand il n’y a pas d’étudiant — et main.py traduit cela en 404. C’est tout le rôle de la couche HTTP.
Exercice 3: Une requête — de bout en bout
Remettez dans l’ordre ce qui se passe dans la requête GET /students/2 :
- (a)
db.get_student(2)exécuteSELECT ... WHERE id = %savec(2,) - (b) FastAPI convertit le
"2"du chemin enint - (c) La réponse revient en JSON avec 200
- (d)
main.get_student(2)est appelée - (e) La ligne revient sous forme de dictionnaire
Afficher la solution
L’ordre : (b), (d), (a), (e), (c)
GET /students/2
-> FastAPI: student_id = 2 (int)
-> main.get_student(2)
-> db.get_student(2)
-> SELECT ... WHERE id = %s (2,)
-> {'id': 2, 'name': 'Avi', ...}
-> JSON, 200Exercice 4: Un endpoint de recherche
Ajoutez à main.py le GET /students/search?name=..., qui utilise db.search_students. Un nom de moins de 2 caractères → 400. Où faut-il définir la route, et pourquoi ?
Afficher la solution
main.py (en plus des routes précédentes)
@app.get("/students/search")
def search_students(name: str):
if len(name.strip()) < 2:
raise HTTPException(status_code=400, detail="too short")
return db.search_students(name)?name=av→ 200 avec la liste.?name=a→ 400. Sansname→ 422 (une query obligatoire, car elle n’a pas de valeur par défaut).- Définissez
/students/searchavant/students/{student_id}— sinon FastAPI essaiera de convertir"search"en int et renverra 422.
Exercice 5: Qu’est-ce qui ne va pas ?
@app.get("/students/{student_id}")
def get_student(student_id: int):
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
return cursor.fetchone()Trouvez trois problèmes dans ce code.
Afficher la solution
- Du SQL dans la route — un mélange des couches. Si nous changeons la structure de la table, il faudra chercher du SQL dans toutes les routes. Sa place est dans
db.py. - La connexion n’est pas fermée — il n’y a pas de
close(), et certainement pas dansfinally. Sur un serveur qui tourne en permanence, cela s’accumule. - Un étudiant qui n’existe pas revient en 200 avec
null— il faut vérifierNoneet renvoyer 404. Et en plus, sansdictionary=True, l’étudiant revient sous forme de liste sans noms de champs, au lieu d’un dictionnaire.
La correction : return db.get_student(student_id) avec une vérification de None — comme dans le deuxième exercice.
Résumé
main.py= la couche API : les routes, la validation et les codes d’état.db.py= la couche de données : SQL, les connexions et commit. Pas de SQL dans les routes.- La correspondance : GET→SELECT, POST→INSERT (201), PUT→UPDATE, DELETE→DELETE (204).
None/False→ 404.ValueError→ 409. - Route = entrée → un appel à db → traduction en HTTP.
db.init_db()une seule fois au démarrage du serveur (IF NOT EXISTS).- Les chemins fixes (
/students/search) avant/students/{student_id}. - Les données survivent au redémarrage du serveur, car elles sont dans la base de données.