# FastAPI : Path Params, query, body et HTTPException<no value>

## Qu'allons-nous apprendre ?

Dans la leçon précédente, les fonctions ne recevaient aucune entrée. Maintenant, elles vont recevoir les trois types que nous avons appris :

| Entrée | Comment FastAPI la reconnaît |
|---|---|
| **Path Parameter** | Un nom entre accolades dans le chemin : `"/students/{student_id}"` |
| **Query Parameter** | Un paramètre de la fonction qui n'apparaît pas dans le chemin |
| **Body** | Un paramètre dont le type est un **modèle Pydantic** |

Et en plus : les **Type Hints**, **`status_code`**, **`HTTPException`**, et l'erreur **422** automatique.

## Pourquoi en avons-nous besoin ?

Sans entrée, un serveur ne peut que renvoyer toujours la même chose. Avec une entrée, il peut renvoyer **un étudiant précis**, **filtrer**, et **créer** de nouveaux étudiants. Et un énorme bonus : FastAPI **vérifie et traduit l'entrée pour nous**. Un serveur de calculatrice écrit à la main plantait sur `"hello"`. Ici, une mauvaise entrée recevra automatiquement un **422** avec une explication précise, et le serveur continuera de fonctionner.

## Explication à partir des bases

### Les Type Hints

**Type Hint** = une annotation dans le code qui indique le **type attendu** d'un paramètre :

```python
def get_student(student_id: int):
```

`student_id: int` = « ce paramètre est censé être un `int` ». En Python ordinaire, les Type Hints **ne sont pas imposés** : ce n'est que de la documentation, qui aide VS Code et les développeurs. **Mais FastAPI les lit et les utilise** :

1. **La conversion** — la chaîne `"7"` arrive de l'URL, et FastAPI la convertit en `7`, car nous avons demandé un `int`.
2. **La validation** — si `"abc"` est arrivé, FastAPI n'appelle **pas** la fonction, mais renvoie un **422** avec une explication.
3. **La documentation** — `/docs` affiche le type de chaque paramètre.

### Path Parameter

```python
@app.get("/students/{student_id}")
def get_student(student_id: int):
```

`{student_id}` dans le chemin = « cette partie change ». FastAPI la passe au paramètre qui porte **exactement le même nom**.

### Query Parameters

**Tout paramètre de la fonction qui n'apparaît pas dans le chemin (et qui n'est pas un modèle) = un Query Parameter.**

```python
def list_students(city: str | None = None, min_age: int = 0, limit: int = 10):
```

| Définition | Signification |
|---|---|
| `min_age: int = 0` | **Facultatif**, avec une valeur par défaut de 0. |
| `limit: int` (sans `=`) | **Obligatoire**. Sans lui → 422. |
| `city: str \| None = None` | **Facultatif, sans valeur par défaut** : « soit une chaîne, soit `None` ». `None` = « il n'a pas été envoyé, donc on ne filtre pas ». |

`str | None` se lit « str **ou** None ». C'est un Type Hint qui dit que deux types sont permis.

### Le body avec Pydantic

**Pydantic** est une bibliothèque (installée avec FastAPI) qui définit **une structure de données** avec des types. On crée une **classe** qui hérite de `BaseModel` (l'héritage, le thème POO !) :

```python
from pydantic import BaseModel

class StudentIn(BaseModel):
    name: str
    age: int
    city: str = "Unknown"
```

- Chaque ligne = **un champ**, avec un type. Un champ avec `=` est facultatif, avec une valeur par défaut.
- **Il n'y a pas de `__init__` !** `BaseModel` le construit tout seul à partir des champs.
- Quand le paramètre d'une fonction est de ce type, FastAPI comprend que c'est un **body** : il lit le JSON, vérifie que tous les champs existent et sont du bon type, et construit un objet. On accède aux champs avec un point : `student.name`.

{{< box "analogy" "Analogie : un formulaire avec des champs obligatoires" >}}
Un modèle Pydantic est **un formulaire imprimé** : « Nom (obligatoire, texte). Âge (obligatoire, un nombre). Ville (facultatif) ». L'employé (FastAPI) ne transmet pas pour traitement (à votre fonction) un formulaire auquel il manque un champ obligatoire, ou dans lequel on a écrit « vingt » dans le champ de l'âge. Il le rend immédiatement à celui qui l'a soumis (**422**) avec le champ problématique marqué. Votre fonction ne reçoit **que des formulaires valides**.
{{< /box >}}

### `status_code` et `HTTPException`

- **`@app.post("/students", status_code=201)`** — le code de succès de cet endpoint est **201 Created**, et non 200.
- **`raise HTTPException(status_code=404, detail="...")`** — arrête la fonction et renvoie un code d'erreur au client avec le JSON `{"detail": "..."}`. C'est un `raise` ordinaire (souvenez-vous du thème des exceptions !), et FastAPI l'attrape et le transforme en réponse HTTP.

## Syntaxe

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class ItemIn(BaseModel):              # body model
    name: str
    price: float
    in_stock: bool = True

@app.get("/items/{item_id}")          # path parameter
def get_item(item_id: int):
    ...
    raise HTTPException(status_code=404, detail="item not found")

@app.get("/items")                    # query parameters
def list_items(max_price: float | None = None, limit: int = 10):
    ...

@app.post("/items", status_code=201)  # body
def create_item(item: ItemIn):
    item.name, item.price             # use the validated fields
```

## Premier exemple de code

**Une API d'étudiants en mémoire** (en RAM. Il n'y a pas encore de base de données, elle viendra dans le thème des bases de données). `week-4/hello-api/students_memory.py` :

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Students (in memory)")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
    {"id": 3, "name": "Noa", "age": 22, "city": "Haifa"},
]

class StudentIn(BaseModel):
    name: str
    age: int
    city: str = "Unknown"

@app.get("/students")
def list_students(city: str | None = None, min_age: int = 0, limit: int = 10):
    result = []
    for student in students:
        if city is not None and student["city"] != city:
            continue
        if student["age"] < min_age:
            continue
        result.append(student)
    return result[:limit]

@app.get("/students/{student_id}")
def get_student(student_id: int):
    for student in students:
        if student["id"] == student_id:
            return student
    raise HTTPException(status_code=404, detail="student " + str(student_id) + " not found")

@app.post("/students", status_code=201)
def create_student(student: StudentIn):
    new_id = max(s["id"] for s in students) + 1
    new_student = {"id": new_id, "name": student.name, "age": student.age, "city": student.city}
    students.append(new_student)
    return new_student
```

**Le lancement :** `uvicorn students_memory:app --reload` (remarquez : le nom du fichier a changé !).

## Analyse du code ligne par ligne

| Code | Explication |
|---|---|
| `students = [...]` | Une « base de données » temporaire en RAM : une liste de dicts. **Elle est effacée à chaque redémarrage** (souvenez-vous de la RAM du thème sur l'ordinateur !). Dans le thème des bases de données, nous passerons à une vraie base de données. |
| `class StudentIn(BaseModel):` | Le modèle du body : ce que le client **envoie** à la création. Il n'y a pas d'`id` dedans, car c'est le serveur qui fixe l'id. |
| `city: str = "Unknown"` | Un champ facultatif dans le body. |
| `list_students(city: str \| None = None, min_age: int = 0, limit: int = 10)` | Trois **Query Parameters**, tous facultatifs. `/students?city=Haifa&min_age=23`. |
| `if city is not None and ...: continue` | Filtrer seulement si `city` a été envoyé. `continue` passe à l'étudiant suivant. |
| `return result[:limit]` | Le Slicing : au plus `limit` résultats. |
| `@app.get("/students/{student_id}")` + `student_id: int` | Un **Path Parameter**, avec conversion en `int`. |
| `raise HTTPException(status_code=404, ...)` | Introuvable, donc un 404 avec un message. |
| `@app.post(..., status_code=201)` | La création renvoie **201**. |
| `create_student(student: StudentIn)` | Le type est un modèle, c'est donc un **body**. FastAPI le décode et le vérifie. |
| `student.name`, `student.age` | L'accès aux champs d'un objet (le thème POO). |
| `max(s["id"] for s in students) + 1` | Un nouvel identifiant : le plus grand + 1. |

## Un autre exemple

**Un test complet de tous les types d'entrée, et les vraies réponses.** (Vous pouvez aussi tester dans `/docs`, ou avec curl.)

| Requête | Code | Réponse |
|---|---|---|
| `GET /students` | 200 | Les trois |
| `GET /students?city=Haifa` | 200 | Dana et Noa |
| `GET /students?min_age=23&limit=1` | 200 | `[{"id": 1, "name": "Dana", ...}]` |
| `GET /students/2` | 200 | `{"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"}` |
| `GET /students/42` | **404** | `{"detail": "student 42 not found"}` |
| `GET /students/abc` | **422** | Voir ci-dessous |
| `GET /students?min_age=old` | **422** | Voir ci-dessous |
| `POST /students` + `{"name": "Tom", "age": 27}` | **201** | `{"id": 4, "name": "Tom", "age": 27, "city": "Unknown"}` |
| `POST /students` + `{"name": "Rina", "age": "twenty"}` | **422** | Voir ci-dessous |

**L'erreur 422 — à quoi elle ressemble** (`GET /students/abc`) :

```json
{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["path", "student_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "abc"
    }
  ]
}
```

**Lire un 422 :**

- `loc` (Location) — **où** est le problème : `["path", "student_id"]`. Dans la query, ce serait `["query", "min_age"]`, et dans le body `["body", "age"]`.
- `msg` — **quel** est le problème.
- `input` — **ce qui** a réellement été reçu.
- **Notre fonction ne s'est pas exécutée du tout.** FastAPI a arrêté la requête avant. Pas besoin de `try`/`except` autour de `int()`.

**Et après le `POST` de Tom :** `GET /students?city=Unknown` renvoie Tom, car la ville a reçu la valeur par défaut. Mais **arrêtez le serveur et relancez-le** : Tom **a disparu**. Les données étaient en RAM. C'est exactement la raison d'être d'une base de données (c'est pour bientôt !).

## Erreur courante

**Erreur 1 : un nom qui ne correspond pas entre le chemin et le paramètre.**

```python
@app.get("/students/{student_id}")
def get_student(id: int):
```

FastAPI ne trouve pas de paramètre nommé `student_id`, et traite `id` comme un **Query Parameter obligatoire**. Chaque requête vers `/students/7` renverra un 422 (`"loc": ["query", "id"]`, manquant). **Le nom entre accolades doit être identique au nom du paramètre.**

**Erreur 2 : un Path Parameter sans Type Hint.**

```python
def get_student(student_id):
    for student in students:
        if student["id"] == student_id:
```

Sans `: int`, le paramètre arrive sous forme de **chaîne** `"2"`, et `2 == "2"` vaut `False`. Le résultat : toujours 404, sans aucune erreur. **Une Logical Error silencieuse.**

**Erreur 3 : l'ordre des routes avec un chemin fixe et un chemin avec paramètre.**

```python
@app.get("/students/{student_id}")
def get_student(student_id: int): ...

@app.get("/students/top")
def top_students(): ...
```

`GET /students/top` → **422** ! FastAPI vérifie les routes **dans l'ordre**, et `/students/{student_id}` « attrape » `top` comme student_id, et échoue à le convertir en int. **Les chemins fixes avant les chemins avec paramètres.**

**Erreur 4 : `return` au lieu de `raise` pour une HTTPException.**

```python
return HTTPException(status_code=404, detail="not found")
```

Renvoie **200**, avec un objet étrange dans le JSON. Une erreur se **lance** (`raise`), elle ne se renvoie pas.

**Erreur 5 : inclure `id` dans le modèle du body.** Si `StudentIn` contient `id: int`, le client **doit** envoyer un id, et peut envoyer un id qui existe déjà. C'est **le serveur** qui fixe l'identifiant. Le modèle du body ne contient que ce que le client est censé envoyer.

<!-- 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: Un Path Parameter avec un Type Hint

Écrivez une API avec une liste d'étudiants en mémoire (des dictionnaires avec `id`, `name`, `age`, `city`), et une route `GET /students/{student_id}` qui renvoie l'étudiant, ou 404 avec un `detail` valant `"student not found"`.

1. Que se passera-t-il avec la requête `GET /students/abc` ?

{{< solution >}}
```python
from fastapi import FastAPI, HTTPException

app = FastAPI(title="Students API")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]


@app.get("/students/{student_id}")
def get_student(student_id: int):
    for student in students:
        if student["id"] == student_id:
            return student
    raise HTTPException(status_code=404, detail="student not found")
```

- `GET /students/2` → 200 avec Avi. `GET /students/9` → **404** `{"detail": "student not found"}`.
- `GET /students/abc` → **422** : le Type Hint `student_id: int` fait que FastAPI vérifie et convertit tout seul. La fonction ne s'exécute pas du tout.
{{< /solution >}}

### Exercice 2: Les Query Parameters

Ajoutez une route `GET /students` qui reçoit un Query Parameter facultatif `city` (valeur par défaut `None`), et ne renvoie que les étudiants de cette ville — ou tous si aucune ville n'a été envoyée.

{{< solution >}}
```python
from fastapi import FastAPI

app = FastAPI(title="Students API")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]


@app.get("/students")
def list_students(city: str = None):
    if city is None:
        return students
    result = []
    for student in students:
        if student["city"] == city:
            result.append(student)
    return result
```

- `GET /students` → les deux étudiants.
- `GET /students?city=Haifa` → seulement Dana.

Un paramètre de la fonction qui ne fait **pas** partie du chemin devient automatiquement un **Query Parameter**. Une valeur par défaut le rend facultatif.
{{< /solution >}}

### Exercice 3: Un POST avec un body

Ajoutez un modèle Pydantic nommé `StudentIn` (`name`, `age`, `city`) et une route `POST /students` qui renvoie 201, donne à l'étudiant un nouvel `id` et l'ajoute à la liste. Un âge inférieur à 16 → 400.

{{< solution >}}
```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Students API")

MIN_AGE = 16

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
]


class StudentIn(BaseModel):
    name: str
    age: int
    city: str


@app.post("/students", status_code=201)
def create_student(student: StudentIn):
    if student.age < MIN_AGE:
        raise HTTPException(status_code=400, detail="age must be at least 16")
    new_student = {"id": len(students) + 1, "name": student.name,
                   "age": student.age, "city": student.city}
    students.append(new_student)
    return new_student
```

- `POST /students` avec `{"name": "Noa", "age": 22, "city": "Eilat"}` → **201** avec `id: 2`.
- L'âge 12 → **400**. Un corps sans `age` → **422** — Pydantic rejette une mauvaise structure avant que la fonction ne s'exécute.
{{< /solution >}}

### Exercice 4: 422 face à 400

1. Quelle est la différence entre 422 et 400 dans FastAPI ? Qui renvoie chacun ?
2. Pour chaque requête — quel code reviendra ?

```text
POST /students  {"name": "Noa", "age": "abc", "city": "Eilat"}
POST /students  {"name": "Noa", "age": 10, "city": "Eilat"}
POST /students  {"name": "Noa", "city": "Eilat"}
```

{{< solution >}}
1. **422** — **FastAPI et Pydantic** le renvoient automatiquement quand la structure ou le type est faux. **400** — **notre code** le renvoie quand la valeur est valide du point de vue du type, mais ne respecte pas une règle métier.
2. Les réponses :
   - `"age": "abc"` → **422** (l'âge n'est pas un nombre).
   - `"age": 10` → **400** (un nombre valide, mais en dessous de l'âge minimum — notre règle).
   - Sans `age` → **422** (un champ obligatoire manque).
{{< /solution >}}

### Exercice 5: Des statistiques

Ajoutez une route `GET /students/stats` qui renvoie le nombre d'étudiants et l'âge moyen (arrondi à un chiffre). Où faut-il la définir par rapport à `GET /students/{student_id}`, et pourquoi ?

{{< solution >}}
```python
from fastapi import FastAPI, HTTPException

app = FastAPI(title="Students API")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]


@app.get("/students/stats")
def students_stats():
    total_age = 0
    for student in students:
        total_age = total_age + student["age"]
    return {"count": len(students), "average_age": round(total_age / len(students), 1)}


@app.get("/students/{student_id}")
def get_student(student_id: int):
    for student in students:
        if student["id"] == student_id:
            return student
    raise HTTPException(status_code=404, detail="student not found")
```

`GET /students/stats` → `{"count": 2, "average_age": 27.5}`.

Définissez `/students/stats` **avant** `/students/{student_id}`. FastAPI vérifie les routes dans l'ordre — si `{student_id}` venait en premier, `stats` serait traité comme un id, et échouerait avec un 422 parce que ce n'est pas un nombre.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Les Type Hints** (`x: int`) — FastAPI convertit et vérifie selon eux. Une entrée invalide reçoit automatiquement un **422**, et la fonction ne s'exécute pas.
- **Path Parameter** : `"/students/{student_id}"` + `student_id: int`, avec exactement le même nom.
- **Query** : tout paramètre qui n'est pas dans le chemin. `x: int = 0` est facultatif. `x: str | None = None` est facultatif sans valeur par défaut. Sans `=`, obligatoire.
- **Body** : une classe qui hérite de `BaseModel` (Pydantic). Un paramètre de ce type = le JSON de la requête, après validation.
- `status_code=201` pour la création. **`raise HTTPException(404, ...)`** pour « introuvable », et 400 pour une règle métier.
- **Les chemins fixes avant les chemins avec paramètre.** Et les données en RAM sont effacées au redémarrage, c'est pourquoi il faut une base de données.

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

## Documentation officielle

- [FastAPI — Path Parameters](https://fastapi.tiangolo.com/tutorial/path-params/)
- [FastAPI — Query Parameters](https://fastapi.tiangolo.com/tutorial/query-params/)
- [FastAPI — Request Body](https://fastapi.tiangolo.com/tutorial/body/)
- [FastAPI — Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/)

<!-- docs:end -->
