# Relier les routes aux actions de la base de données : l'architecture<no value>

## 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

```text
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 lives
```

| | `main.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.py` dans 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ême `db.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

```text
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).

{{< box "analogy" "Analogie : un restaurant, encore" >}}
**`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.
{{< /box >}}

### 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

```python
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`) :

```powershell
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` :

```text
.venv/
__pycache__/
.env
```

**Copiez `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 :

```python
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 student
```

**Ajouter des données pour tester** (la base est vide ! Il n'y a pas encore de POST). `seed.py` :

```python
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)
```

```powershell
python seed.py
uvicorn main:app --reload
```

**Dans le navigateur :**

- `http://127.0.0.1:8000/students`
- `http://127.0.0.1:8000/students/2`
- `http://127.0.0.1:8000/students/99`

**Les réponses :**

```json
[{"id":1,"name":"Dana","age":24,"email":"dana@mail.com"},{"id":2,"name":"Avi","age":31,"email":"avi@mail.com"}]
```

```json
{"id":2,"name":"Avi","age":31,"email":"avi@mail.com"}
```

```json
{"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` :

```python
@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.

```text
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.**

```python
@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.**

```python
@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 :

```text
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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) ?

1. `cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))`
2. `raise HTTPException(status_code=404, detail="student not found")`
3. `@app.get("/students/{student_id}")`
4. `mysql.connector.connect(host="localhost", user="root", ...)`
5. `return db.get_student(student_id)`

{{< solution >}}
1. **db.py** — du SQL.
2. **main.py** — un code HTTP.
3. **main.py** — une route.
4. **db.py** — les informations de connexion à la base.
5. **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.
{{< /solution >}}

### 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"`.

{{< solution >}}
**main.py**

```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 student
```

`db.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.
{{< /solution >}}

### 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écute `SELECT ... WHERE id = %s` avec `(2,)`
- (b) FastAPI convertit le `"2"` du chemin en `int`
- (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

{{< solution >}}
L'ordre : **(b), (d), (a), (e), (c)**

```text
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, 200
```
{{< /solution >}}

### Exercice 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 ?

{{< solution >}}
**main.py** (en plus des routes précédentes)

```py
@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**. Sans `name` → **422** (une query obligatoire, car elle n'a pas de valeur par défaut).
- Définissez `/students/search` **avant** `/students/{student_id}` — sinon FastAPI essaiera de convertir `"search"` en int et renverra 422.
{{< /solution >}}

### Exercice 5: Qu'est-ce qui ne va pas ?

```text
@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.

{{< solution >}}
1. **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`.
2. **La connexion n'est pas fermée** — il n'y a pas de `close()`, et certainement pas dans `finally`. Sur un serveur qui tourne en permanence, cela s'accumule.
3. **Un étudiant qui n'existe pas revient en 200 avec `null`** — il faut vérifier `None` et renvoyer 404. Et en plus, sans `dictionary=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.
{{< /solution >}}

<!-- exercises:end -->

## 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.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [FastAPI — Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/)
- [FastAPI — Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/)

<!-- docs:end -->
