---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# REST et API HTTP

Les **API REST** (Representational State Transfer) dominent l'architecture des services web depuis les années 2000. Proposé par Roy Fielding dans sa thèse de doctorat en 2000, REST n'est pas un protocole mais un **style architectural** définissant un ensemble de contraintes qui, respectées, produisent des systèmes évolutifs, simples et interopérables.

Ce chapitre couvre les principes fondamentaux de REST, la conception d'URL, la sémantique des méthodes HTTP, les codes de statut, la pagination, le versioning, l'authentification, et la spécification OpenAPI. Il inclut un mini serveur REST avec la stdlib Python et des visualisations pédagogiques.

```{code-cell} python
:tags: [hide-input]

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import pandas as pd
import json
import hashlib
import base64
import time
import urllib.request
import urllib.parse
import urllib.error
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)
```

## Les principes architecturaux de REST

### Les six contraintes de Fielding

REST est défini par six contraintes architecturales. Respecter ces contraintes produit un système aux propriétés bien connues :

**1. Client-Serveur** : séparation stricte entre interface utilisateur (client) et stockage de données (serveur). Le client ne se préoccupe pas de la persistance des données ; le serveur ne se préoccupe pas de l'interface.

**2. Sans état (Stateless)** : chaque requête du client doit contenir **toutes les informations** nécessaires à sa compréhension. Le serveur ne conserve aucun contexte de session entre les requêtes. L'état de session est entièrement géré côté client (tokens, cookies).

**3. Cache** : les réponses doivent être qualifiées de cachables ou non-cachables. Un cache bien géré améliore les performances et réduit la charge serveur.

**4. Interface uniforme** : c'est la contrainte centrale de REST. Elle comprend quatre sous-contraintes : identification des ressources dans les requêtes (URI), manipulation des ressources par leurs représentations, messages auto-descriptifs, et hypermédia comme moteur d'état applicatif (HATEOAS).

**5. Système en couches** : le client ne sait pas s'il parle directement au serveur final ou à un intermédiaire (load balancer, cache, proxy). Cela permet la scalabilité et la sécurité.

**6. Code à la demande (optionnel)** : le serveur peut optionnellement envoyer du code exécutable au client (JavaScript, WebAssembly).

### HATEOAS

**HATEOAS** (Hypermedia As The Engine Of Application State) est la contrainte la plus souvent ignorée des API dites "REST". Elle stipule que le client ne doit pas avoir de connaissance préalable des URIs de l'API : le serveur doit inclure dans chaque réponse les liens vers les actions disponibles.

```json
{
  "id": 42,
  "nom": "Jean Dupont",
  "statut": "actif",
  "_links": {
    "self": {"href": "/api/v1/utilisateurs/42"},
    "modifier": {"href": "/api/v1/utilisateurs/42", "method": "PUT"},
    "supprimer": {"href": "/api/v1/utilisateurs/42", "method": "DELETE"},
    "commandes": {"href": "/api/v1/utilisateurs/42/commandes"},
    "factures": {"href": "/api/v1/utilisateurs/42/factures"}
  }
}
```

En pratique, HATEOAS est rarement implémenté complètement. La plupart des APIs modernes sont des **APIs "REST-like"** qui respectent les contraintes client-serveur, stateless, et interface uniforme, mais pas HATEOAS.

```{code-cell} python
:tags: [hide-input]

# Visualisation : les 6 contraintes REST et leurs bénéfices
fig, ax = plt.subplots(figsize=(12, 7))
ax.set_xlim(0, 12)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Les 6 contraintes architecturales de REST (Fielding, 2000)", fontsize=13, fontweight="bold")

contraintes = [
    (1.5, 6.5, "Client-Serveur", "#2196F3",
     "Séparation UI / données\n→ Évolutivité indépendante"),
    (4.5, 6.5, "Sans état\n(Stateless)", "#4CAF50",
     "Pas de session serveur\n→ Scalabilité, fiabilité"),
    (7.5, 6.5, "Cache", "#FF9800",
     "Réponses cachables/non\n→ Performance réseau"),
    (1.5, 3.0, "Interface\nUniforme", "#9C27B0",
     "URI, représentation, HATEOAS\n→ Interopérabilité"),
    (4.5, 3.0, "Système\nen couches", "#F44336",
     "Proxys, LB, cache transparents\n→ Sécurité, scalabilité"),
    (7.5, 3.0, "Code à la\ndemande (opt.)", "#607D8B",
     "JS, WASM envoyé au client\n→ Extensibilité"),
]

for x, y, titre, couleur, benefice in contraintes:
    # Boîte principale
    rect = mpatches.FancyBboxPatch((x - 1.3, y - 0.6), 2.6, 1.2,
                                    boxstyle="round,pad=0.1",
                                    facecolor=couleur, edgecolor="white",
                                    alpha=0.9, linewidth=2, zorder=5)
    ax.add_patch(rect)
    ax.text(x, y, titre, ha="center", va="center", fontsize=9.5,
            color="white", fontweight="bold", zorder=6)
    # Bénéfice
    ax.text(x, y - 1.2, benefice, ha="center", va="top", fontsize=7.5,
            color="#37474F", style="italic", multialignment="center")

# Flèche centrale REST
center_x, center_y = 10.2, 4.75
cercle = plt.Circle((center_x, center_y), 1.2, color="#37474F", alpha=0.15, zorder=3)
ax.add_patch(cercle)
ax.text(center_x, center_y, "API\nREST", ha="center", va="center",
        fontsize=12, fontweight="bold", color="#37474F", zorder=4)

# Flèches vers le centre
for x, y, *_ in contraintes:
    ax.annotate("", xy=(center_x - 1.1, center_y + (y - center_y)*0.2),
                xytext=(x + 1.4 if x < 6 else x - 1.4, y),
                arrowprops=dict(arrowstyle="->", color="#9E9E9E", lw=1,
                               connectionstyle="arc3,rad=0.1"))

ax.text(6, 0.5,
        "Une API est vraiment RESTful si elle respecte toutes ces contraintes. "
        "En pratique, on parle souvent d'API HTTP ou API REST-like.",
        ha="center", fontsize=9, color="#607D8B", style="italic")

plt.tight_layout()
plt.show()
```

## Conception d'URL et nommage des ressources

### Ressources et représentations

En REST, tout est **ressource** : un utilisateur, une commande, un article, une image. Chaque ressource est identifiée par une **URI** (Uniform Resource Identifier). Une ressource peut avoir plusieurs **représentations** (JSON, XML, HTML) négociées via l'en-tête `Accept`.

**Règles de nommage des URIs** :
- Utiliser des **noms** (pas des verbes) : `/utilisateurs` et non `/obtenirUtilisateurs`
- Utiliser des **pluriels** pour les collections : `/utilisateurs`, `/commandes`
- Utiliser des **minuscules** et des tirets pour les séparateurs de mots : `/articles-de-blog`
- Éviter les extensions de fichiers : `/utilisateurs/42` et non `/utilisateurs/42.json`
- La hiérarchie reflète les relations : `/utilisateurs/42/commandes` (commandes d'un utilisateur)

**Exemples de conception d'URL** :

| Ressource | URI | Méthode | Action |
|-----------|-----|---------|--------|
| Collection utilisateurs | `/api/v1/utilisateurs` | GET | Lister |
| Créer un utilisateur | `/api/v1/utilisateurs` | POST | Créer |
| Utilisateur spécifique | `/api/v1/utilisateurs/42` | GET | Lire |
| Modifier un utilisateur | `/api/v1/utilisateurs/42` | PUT | Remplacer |
| Mise à jour partielle | `/api/v1/utilisateurs/42` | PATCH | Modifier |
| Supprimer | `/api/v1/utilisateurs/42` | DELETE | Supprimer |
| Commandes d'un utilisateur | `/api/v1/utilisateurs/42/commandes` | GET | Lister |

### Paramètres de requête

Les paramètres de requête servent à :
- **Filtrage** : `GET /articles?categorie=tech&statut=publie`
- **Tri** : `GET /utilisateurs?tri=nom&ordre=asc`
- **Pagination** : `GET /commandes?page=3&limite=20`
- **Recherche** : `GET /produits?q=ordinateur+portable`
- **Sélection de champs** : `GET /utilisateurs?champs=id,nom,email`

```{admonition} URIs des actions non-CRUD
:class: note

Certaines opérations ne correspondent pas aux actions CRUD standards. Deux approches :
1. **Sous-ressource verbale** : `POST /commandes/42/annuler` — acceptable quand l'action modifie l'état de la ressource
2. **Utiliser PATCH avec un champ statut** : `PATCH /commandes/42` avec `{"statut": "annule"}` — plus RESTful

Éviter absolument : `GET /annulerCommande?id=42` — viole la sémantique des méthodes HTTP.
```

## Idempotence et sécurité des méthodes HTTP

```{code-cell} python
:tags: [hide-input]

# Tableau des propriétés des méthodes HTTP
fig, ax = plt.subplots(figsize=(13, 5))
ax.axis("off")

colonnes = ["Méthode", "Sûre\n(Safe)", "Idempotente", "Corps\nrequête", "Corps\nréponse", "Cache", "Usage typique"]
lignes = [
    ["GET", "Oui", "Oui", "Non (ignoré)", "Oui", "Oui", "Lire une ressource ou collection"],
    ["HEAD", "Oui", "Oui", "Non", "Non (headers)", "Oui", "Vérifier existence, taille"],
    ["OPTIONS", "Oui", "Oui", "Non", "Oui", "Non", "Méthodes autorisées, CORS preflight"],
    ["POST", "Non", "Non", "Oui", "Oui", "Non", "Créer une ressource"],
    ["PUT", "Non", "Oui", "Oui", "Oui", "Non", "Remplacer une ressource entière"],
    ["PATCH", "Non", "Non*", "Oui", "Oui", "Non", "Modification partielle"],
    ["DELETE", "Non", "Oui", "Optionnel", "Optionnel", "Non", "Supprimer une ressource"],
]

couleurs_lignes = []
for i, ligne in enumerate(lignes):
    row = []
    for j, cell in enumerate(ligne):
        if j == 0:
            row.append("#ECEFF1")
        elif cell in ("Oui",):
            row.append("#E8F5E9")
        elif cell in ("Non",):
            row.append("#FFEBEE")
        elif i % 2 == 0:
            row.append("#FAFAFA")
        else:
            row.append("#F5F5F5")
    couleurs_lignes.append(row)

table = ax.table(
    cellText=lignes,
    colLabels=colonnes,
    cellLoc="center",
    loc="center",
    cellColours=couleurs_lignes,
)
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 1.9)

for j in range(len(colonnes)):
    table[0, j].set_facecolor("#37474F")
    table[0, j].set_text_props(color="white", fontweight="bold")

ax.set_title("Propriétés des méthodes HTTP — Sécurité et idempotence", fontsize=12, fontweight="bold", pad=20)
ax.text(0.5, -0.05, "* PATCH peut être rendu idempotent avec des opérations de type JSON Patch (RFC 6902)",
        ha="center", transform=ax.transAxes, fontsize=8, color="#607D8B", style="italic")
plt.tight_layout()
plt.show()
```

```{admonition} Idempotence vs sécurité
:class: note

- **Sûre (safe)** : la méthode ne modifie pas l'état du serveur. GET, HEAD, OPTIONS sont sûres.
- **Idempotente** : effectuer la même opération N fois produit le même résultat qu'une seule fois. DELETE /utilisateurs/42 effectué deux fois : la deuxième fois, la ressource est déjà absente (404), mais l'état du serveur est le même.
- **POST n'est ni sûre ni idempotente** : deux POST identiques créent deux ressources distinctes.
```

## Codes de statut HTTP appropriés

Utiliser les codes de statut HTTP corrects est essentiel pour une API RESTful. Les clients (humains ou machines) s'appuient sur ces codes pour comprendre le résultat de chaque requête.

```{code-cell} python
:tags: [hide-input]

# Visualisation des codes de statut HTTP pour les APIs REST
fig, axes = plt.subplots(1, 5, figsize=(15, 6))
fig.suptitle("Codes de statut HTTP pour APIs REST", fontsize=13, fontweight="bold")

groupes = {
    "2xx\nSuccès": {
        "color": "#4CAF50",
        "codes": [
            ("200", "OK", "Lecture réussie"),
            ("201", "Created", "Ressource créée"),
            ("202", "Accepted", "Traitement asynchrone"),
            ("204", "No Content", "Succès sans contenu\n(DELETE réussi)"),
            ("206", "Partial Content", "Réponse paginée\nou Range"),
        ]
    },
    "3xx\nRedirection": {
        "color": "#FF9800",
        "codes": [
            ("301", "Moved\nPermanently", "URI changé\ndéfinitivement"),
            ("302", "Found", "Redirection\ntemporaire"),
            ("304", "Not Modified", "Cache valide\n(ETag/Last-Modified)"),
            ("307", "Temporary\nRedirect", "Même méthode,\nURI temp."),
            ("308", "Permanent\nRedirect", "Même méthode,\nURI perm."),
        ]
    },
    "4xx\nErreur client": {
        "color": "#F44336",
        "codes": [
            ("400", "Bad Request", "Corps invalide,\nformat incorrect"),
            ("401", "Unauthorized", "Authentification\nrequise"),
            ("403", "Forbidden", "Accès refusé\n(auth OK, droits non)"),
            ("404", "Not Found", "Ressource\ninexistante"),
            ("405", "Method Not\nAllowed", "Méthode HTTP\nnon autorisée"),
            ("409", "Conflict", "Conflit état\n(doublon)"),
            ("410", "Gone", "Supprimée\ndéfinitivement"),
            ("422", "Unprocessable\nEntity", "Validation\nmétier échoue"),
            ("429", "Too Many\nRequests", "Rate limiting\ndépassé"),
        ]
    },
    "5xx\nErreur serveur": {
        "color": "#9C27B0",
        "codes": [
            ("500", "Internal\nServer Error", "Erreur générique\nserveur"),
            ("501", "Not\nImplemented", "Fonctionnalité\nnon dispo"),
            ("502", "Bad Gateway", "Erreur proxy/\nupstream"),
            ("503", "Service\nUnavailable", "Serveur\nsurchargé"),
            ("504", "Gateway\nTimeout", "Timeout upstream"),
        ]
    },
}

for ax, (titre, groupe) in zip(axes, groupes.items()):
    color = groupe["color"]
    codes = groupe["codes"]
    ax.set_xlim(0, 4)
    ax.set_ylim(-0.5, len(codes))
    ax.axis("off")
    ax.set_title(titre, fontsize=11, fontweight="bold", color=color)

    for i, (code, nom, desc) in enumerate(codes):
        y = len(codes) - 1 - i
        rect = mpatches.FancyBboxPatch((0.1, y - 0.38), 3.8, 0.76,
                                        boxstyle="round,pad=0.05",
                                        facecolor=color, edgecolor="white",
                                        alpha=0.15 + 0.05 * (i % 2),
                                        linewidth=1)
        ax.add_patch(rect)
        ax.text(0.4, y + 0.1, code, fontsize=11, fontweight="bold", color=color, va="center")
        ax.text(0.4, y - 0.2, nom, fontsize=7, color="#424242", va="center")
        ax.text(2.1, y, desc, fontsize=6.5, color="#616161", va="center",
                multialignment="left")

# 1xx
ax5 = axes[4]
ax5.set_xlim(0, 4)
ax5.set_ylim(-0.5, 5)
ax5.axis("off")
ax5.set_title("1xx\nInformatif", fontsize=11, fontweight="bold", color="#2196F3")
codes_1xx = [
    ("100", "Continue", "Le client peut\ncontinuer"),
    ("101", "Switching\nProtocols", "Upgrade WebSocket,\nHTTP/2"),
]
for i, (code, nom, desc) in enumerate(codes_1xx):
    y = 4 - i
    rect = mpatches.FancyBboxPatch((0.1, y - 0.38), 3.8, 0.76,
                                    boxstyle="round,pad=0.05",
                                    facecolor="#2196F3", edgecolor="white",
                                    alpha=0.2, linewidth=1)
    ax5.add_patch(rect)
    ax5.text(0.4, y + 0.1, code, fontsize=11, fontweight="bold", color="#2196F3", va="center")
    ax5.text(0.4, y - 0.2, nom, fontsize=7, color="#424242", va="center")
    ax5.text(2.1, y, desc, fontsize=6.5, color="#616161", va="center", multialignment="left")

plt.tight_layout()
plt.show()
```

## Pagination

Renvoyer l'intégralité d'une collection peut être coûteux voire impossible pour des collections de millions d'éléments. La pagination est indispensable.

### Pagination offset/limit

```
GET /api/v1/articles?offset=40&limit=20
```
Retourne les articles 41 à 60. Simple à implémenter, mais présente des problèmes avec les données dynamiques (si un élément est inséré entre deux pages, des éléments peuvent être sautés ou dupliqués).

Réponse avec métadonnées :
```json
{
  "data": [...],
  "pagination": {
    "offset": 40,
    "limit": 20,
    "total": 1247,
    "pages": 63
  }
}
```

### Pagination par curseur (cursor-based)

Plus robuste pour les données dynamiques. Le curseur est un identifiant opaque pointant vers un élément spécifique :

```
GET /api/v1/articles?apres=eyJpZCI6NDF9&limite=20
```

Réponse :
```json
{
  "data": [...],
  "pagination": {
    "curseur_suivant": "eyJpZCI6NjF9",
    "curseur_precedent": "eyJpZCI6MjF9",
    "a_suivant": true,
    "a_precedent": true
  }
}
```

### Link header (RFC 5988)

Le standard HTTP définit l'en-tête `Link` pour la navigation paginée, utilisé par GitHub API :

```http
Link: <https://api.example.com/articles?page=3>; rel="next",
      <https://api.example.com/articles?page=1>; rel="first",
      <https://api.example.com/articles?page=63>; rel="last"
```

## Versioning d'API

Les APIs évoluent. Le versioning permet d'introduire des changements incompatibles sans casser les clients existants.

```{code-cell} python
:tags: [hide-input]

# Comparaison des stratégies de versioning
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
fig.suptitle("Stratégies de versioning d'API", fontsize=13, fontweight="bold")

strategies = [
    {
        "titre": "Versioning par URL",
        "exemple": "GET /api/v2/utilisateurs/42",
        "pros": ["Simple et visible", "Facile à tester\ndans un navigateur",
                 "Facile à router\npar l'infrastructure", "Très répandu"],
        "cons": ["L'URI devrait identifier\nune ressource, pas une version",
                 "Prolifération d'URLs"],
        "color": "#2196F3",
    },
    {
        "titre": "Versioning par en-tête Accept",
        "exemple": "Accept: application/vnd.example.v2+json",
        "pros": ["URIs propres", "Conforme à REST\n(négociation de contenu)",
                 "Peut coexister avec\nd'autres négociations"],
        "cons": ["Moins visible", "Plus difficile à tester\n(curl, navigateur)",
                 "Complexité serveur"],
        "color": "#4CAF50",
    },
    {
        "titre": "Versioning par paramètre",
        "exemple": "GET /api/utilisateurs/42?version=2",
        "pros": ["Simple à ajouter", "Facile à tester"],
        "cons": ["Mélange filtrage et\nversioning dans les params",
                 "Peu conventionnel",
                 "Paramètre peut être\ncaché par des caches"],
        "color": "#FF9800",
    },
]

for ax, strat in zip(axes, strategies):
    ax.set_xlim(0, 5)
    ax.set_ylim(-1, 10)
    ax.axis("off")
    ax.set_title(strat["titre"], fontsize=10.5, fontweight="bold", color=strat["color"])

    # Exemple d'URL
    rect = mpatches.FancyBboxPatch((0.2, 8.2), 4.6, 1.0,
                                    boxstyle="round,pad=0.1",
                                    facecolor=strat["color"], alpha=0.15,
                                    edgecolor=strat["color"], linewidth=1.5)
    ax.add_patch(rect)
    ax.text(2.5, 8.7, strat["exemple"], ha="center", va="center",
            fontsize=7.5, color=strat["color"], fontweight="bold",
            family="monospace")

    # Avantages
    ax.text(2.5, 7.6, "Avantages", ha="center", fontsize=9,
            fontweight="bold", color="#2E7D32")
    for i, pro in enumerate(strat["pros"]):
        ax.text(0.4, 7.0 - i * 1.1, f"✓  {pro}", fontsize=8, color="#2E7D32",
                va="top")

    # Inconvénients
    y_cons = 7.0 - len(strat["pros"]) * 1.1 - 0.6
    ax.text(2.5, y_cons + 0.3, "Inconvénients", ha="center", fontsize=9,
            fontweight="bold", color="#C62828")
    for i, con in enumerate(strat["cons"]):
        ax.text(0.4, y_cons - 0.1 - i * 1.1, f"✗  {con}", fontsize=8,
                color="#C62828", va="top")

plt.tight_layout()
plt.show()
```

## Authentification et autorisation

### Basic Authentication

L'authentification HTTP Basic encode `identifiant:mot_de_passe` en Base64 dans l'en-tête `Authorization`. Elle est simple mais **doit toujours être utilisée sur HTTPS** car le Base64 n'est pas du chiffrement.

```http
Authorization: Basic dXNlcjpwYXNzd29yZA==
```

### Bearer Token (JWT)

Les **JSON Web Tokens (JWT)** sont le mécanisme d'authentification standard des APIs modernes. Un JWT est un token signé contenant des claims (revendications) :

```
Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxNTAwMDAwMH0.xxxx
```

```http
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
```

Le serveur vérifie la signature JWT sans accéder à une base de données, ce qui est stateless et scalable.

### OAuth 2.0 : Authorization Code Flow

OAuth 2.0 est le standard d'autorisation pour permettre à une application tierce d'accéder aux ressources d'un utilisateur sans que cet utilisateur partage son mot de passe.

```{code-cell} python
:tags: [hide-input]

# Diagramme flux OAuth 2.0 Authorization Code
fig, ax = plt.subplots(figsize=(13, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 11)
ax.axis("off")
ax.set_title("Flux OAuth 2.0 — Authorization Code Flow", fontsize=13, fontweight="bold")

# Acteurs
acteurs = [
    (1.5, "Utilisateur\n(Resource Owner)", "#2196F3"),
    (5.5, "Application\n(Client)", "#4CAF50"),
    (9.5, "Serveur\nd'autorisation", "#FF9800"),
    (13, "Serveur\nde ressources", "#9C27B0"),
]

for x, label, color in acteurs:
    ax.axvline(x=x, color=color, linewidth=2, ymin=0.05, ymax=0.9, alpha=0.7)
    ax.text(x, 10.7, label, ha="center", va="center", fontsize=9,
            fontweight="bold", color=color, multialignment="center")

# Messages
messages = [
    # (y, x1, x2, label, color)
    (9.5,  5.5, 1.5, "1. Clique sur 'Connecter avec Google'", "#4CAF50"),
    (8.5,  5.5, 9.5, "2. Redirect → /authorize?client_id=...&redirect_uri=...&scope=...&state=...", "#FF9800"),
    (7.5,  9.5, 1.5, "3. Page de connexion Google", "#FF9800"),
    (6.5,  1.5, 9.5, "4. Identifiants (login/password)", "#2196F3"),
    (5.5,  9.5, 1.5, "5. Redirect → /callback?code=AUTH_CODE&state=...", "#FF9800"),
    (4.5,  1.5, 5.5, "6. Suit la redirection (code fourni)", "#2196F3"),
    (3.5,  5.5, 9.5, "7. POST /token {code, client_secret, redirect_uri}", "#4CAF50"),
    (2.5,  9.5, 5.5, "8. {access_token, refresh_token, expires_in}", "#FF9800"),
    (1.5,  5.5, 13,  "9. GET /userinfo (Authorization: Bearer ACCESS_TOKEN)", "#4CAF50"),
    (0.7,  13,  5.5, "10. {id, email, nom, ...}", "#9C27B0"),
]

for y, x1, x2, label, color in messages:
    ax.annotate("", xy=(x2, y), xytext=(x1, y),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.8))
    mid = (x1 + x2) / 2
    ax.text(mid, y + 0.15, label, ha="center", fontsize=6.8, color=color,
            style="italic")

plt.tight_layout()
plt.show()
```

```{code-cell} python
:tags: [hide-input]

# Simulation : vérification d'un JWT
import json
import base64
import hashlib
import hmac

def base64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")

def base64url_decode(s: str) -> bytes:
    padding = 4 - len(s) % 4
    if padding != 4:
        s += "=" * padding
    return base64.urlsafe_b64decode(s)

def creer_jwt(payload: dict, secret: str) -> str:
    """Crée un JWT signé HS256."""
    header = {"alg": "HS256", "typ": "JWT"}
    header_b64 = base64url_encode(json.dumps(header, separators=(",", ":")).encode())
    payload_b64 = base64url_encode(json.dumps(payload, separators=(",", ":")).encode())
    message = f"{header_b64}.{payload_b64}"
    signature = hmac.new(
        secret.encode(),
        message.encode(),
        hashlib.sha256
    ).digest()
    sig_b64 = base64url_encode(signature)
    return f"{message}.{sig_b64}"

def verifier_jwt(token: str, secret: str) -> dict | None:
    """Vérifie un JWT et retourne le payload si valide."""
    try:
        header_b64, payload_b64, sig_b64 = token.split(".")
        message = f"{header_b64}.{payload_b64}"
        expected_sig = hmac.new(
            secret.encode(),
            message.encode(),
            hashlib.sha256
        ).digest()
        actual_sig = base64url_decode(sig_b64)
        if not hmac.compare_digest(expected_sig, actual_sig):
            return None
        return json.loads(base64url_decode(payload_b64))
    except Exception:
        return None

# Démonstration
secret = "ma_cle_secrete_super_longue_et_complexe"
payload = {
    "sub": "42",
    "nom": "Jean Dupont",
    "role": "admin",
    "iat": 1715000000,
    "exp": 1715086400,
}

token = creer_jwt(payload, secret)
print("JWT généré :")
print(token[:60] + "...")
print()

parties = token.split(".")
print(f"Header   : {json.loads(base64url_decode(parties[0]))}")
print(f"Payload  : {json.loads(base64url_decode(parties[1]))}")
print()

# Vérification
payload_verifie = verifier_jwt(token, secret)
print(f"Signature valide : {payload_verifie is not None}")
print(f"Utilisateur      : {payload_verifie['nom']}, rôle={payload_verifie['role']}")

# Token falsifié
token_falsifie = token[:-5] + "XXXXX"
print(f"\nToken falsifié valide : {verifier_jwt(token_falsifie, secret) is not None}")
```

## OpenAPI et Swagger

**OpenAPI** (anciennement Swagger) est le standard de description des APIs HTTP. Une spécification OpenAPI (YAML ou JSON) décrit complètement l'API : routes, méthodes, paramètres, corps de requête, réponses, schémas de données, sécurité.

```{code-cell} python
:tags: [hide-input]

# Exemple de spécification OpenAPI 3.0 (affichage pédagogique)
openapi_spec = """
openapi: "3.0.3"
info:
  title: API Utilisateurs
  description: API REST pour la gestion des utilisateurs
  version: "1.0.0"
  contact:
    name: Lôc Cosnier
    email: contact@alkimya.fr

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://api-staging.example.com/v1
    description: Staging

paths:
  /utilisateurs:
    get:
      summary: Lister les utilisateurs
      operationId: listerUtilisateurs
      tags: [Utilisateurs]
      security:
        - bearerAuth: []
      parameters:
        - name: page
          in: query
          schema: {type: integer, default: 1}
        - name: limite
          in: query
          schema: {type: integer, default: 20, maximum: 100}
        - name: role
          in: query
          schema: {type: string, enum: [admin, user, moderateur]}
      responses:
        "200":
          description: Liste paginée des utilisateurs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginationUtilisateurs"
        "401":
          $ref: "#/components/responses/NonAuthenrifie"

    post:
      summary: Créer un utilisateur
      operationId: creerUtilisateur
      tags: [Utilisateurs]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NouvelUtilisateur"
      responses:
        "201":
          description: Utilisateur créé
          headers:
            Location:
              schema: {type: string}
              description: URI du nouvel utilisateur
        "409":
          description: Email déjà utilisé

  /utilisateurs/{id}:
    get:
      summary: Obtenir un utilisateur
      operationId: obtenirUtilisateur
      tags: [Utilisateurs]
      parameters:
        - name: id
          in: path
          required: true
          schema: {type: integer}
      responses:
        "200":
          description: Utilisateur trouvé
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Utilisateur"
        "404":
          description: Utilisateur non trouvé

components:
  schemas:
    Utilisateur:
      type: object
      properties:
        id: {type: integer, example: 42}
        nom: {type: string, example: "Jean Dupont"}
        email: {type: string, format: email}
        role: {type: string, enum: [admin, user, moderateur]}
        cree_le: {type: string, format: date-time}
        _links:
          $ref: "#/components/schemas/HATEOASLinks"

    NouvelUtilisateur:
      type: object
      required: [nom, email, mot_de_passe]
      properties:
        nom: {type: string, minLength: 2, maxLength: 100}
        email: {type: string, format: email}
        mot_de_passe: {type: string, minLength: 12}
        role: {type: string, default: user}

    HATEOASLinks:
      type: object
      additionalProperties:
        type: object
        properties:
          href: {type: string}
          method: {type: string}

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
"""

print("Extrait de spécification OpenAPI 3.0 :")
print("=" * 60)
# Afficher seulement les premières lignes pour la lisibilité
for i, line in enumerate(openapi_spec.strip().split("\n")[:50]):
    print(line)
print("  ...")
print(f"\nSpécification complète : {len(openapi_spec.split(chr(10)))} lignes")
```

## Mini serveur REST avec http.server

```{code-cell} python
:tags: [hide-input]

import json
import threading
import time
import urllib.request
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

# Base de données en mémoire
DB = {
    "utilisateurs": {
        1: {"id": 1, "nom": "Alice Martin", "email": "alice@example.com", "role": "admin"},
        2: {"id": 2, "nom": "Bob Durand", "email": "bob@example.com", "role": "user"},
        3: {"id": 3, "nom": "Chloé Petit", "email": "chloe@example.com", "role": "user"},
    },
    "next_id": 4,
}

class RESTHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        pass  # Silence des logs

    def _envoyer_reponse(self, code: int, data, headers: dict = None):
        body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", len(body))
        if headers:
            for k, v in headers.items():
                self.send_header(k, v)
        self.end_headers()
        self.wfile.write(body)

    def _lire_corps(self) -> dict:
        longueur = int(self.headers.get("Content-Length", 0))
        if longueur > 0:
            return json.loads(self.rfile.read(longueur).decode("utf-8"))
        return {}

    def do_GET(self):
        parsed = urlparse(self.path)
        chemin = parsed.path
        params = parse_qs(parsed.query)

        if chemin == "/api/v1/utilisateurs":
            # GET /api/v1/utilisateurs — avec pagination
            limite = int(params.get("limite", [10])[0])
            page = int(params.get("page", [1])[0])
            tous = list(DB["utilisateurs"].values())
            debut = (page - 1) * limite
            fin = debut + limite
            data = {
                "data": tous[debut:fin],
                "pagination": {
                    "page": page, "limite": limite,
                    "total": len(tous),
                    "pages": max(1, -(-len(tous) // limite)),
                }
            }
            self._envoyer_reponse(200, data)

        elif chemin.startswith("/api/v1/utilisateurs/"):
            try:
                uid = int(chemin.split("/")[-1])
            except ValueError:
                self._envoyer_reponse(400, {"erreur": "ID invalide"})
                return
            if uid not in DB["utilisateurs"]:
                self._envoyer_reponse(404, {"erreur": f"Utilisateur {uid} non trouvé"})
                return
            user = DB["utilisateurs"][uid].copy()
            user["_links"] = {
                "self": {"href": f"/api/v1/utilisateurs/{uid}"},
                "modifier": {"href": f"/api/v1/utilisateurs/{uid}", "method": "PUT"},
                "supprimer": {"href": f"/api/v1/utilisateurs/{uid}", "method": "DELETE"},
            }
            self._envoyer_reponse(200, user)
        else:
            self._envoyer_reponse(404, {"erreur": "Route non trouvée"})

    def do_POST(self):
        if self.path == "/api/v1/utilisateurs":
            corps = self._lire_corps()
            champs_requis = ["nom", "email"]
            manquants = [c for c in champs_requis if c not in corps]
            if manquants:
                self._envoyer_reponse(422, {"erreur": f"Champs requis manquants : {manquants}"})
                return

            # Vérification email unique
            emails = [u["email"] for u in DB["utilisateurs"].values()]
            if corps["email"] in emails:
                self._envoyer_reponse(409, {"erreur": "Email déjà utilisé"})
                return

            uid = DB["next_id"]
            DB["next_id"] += 1
            nouvel_user = {"id": uid, "role": "user", **corps}
            DB["utilisateurs"][uid] = nouvel_user
            self._envoyer_reponse(
                201, nouvel_user,
                headers={"Location": f"/api/v1/utilisateurs/{uid}"}
            )
        else:
            self._envoyer_reponse(404, {"erreur": "Route non trouvée"})

    def do_PATCH(self):
        if self.path.startswith("/api/v1/utilisateurs/"):
            try:
                uid = int(self.path.split("/")[-1])
            except ValueError:
                self._envoyer_reponse(400, {"erreur": "ID invalide"})
                return
            if uid not in DB["utilisateurs"]:
                self._envoyer_reponse(404, {"erreur": "Utilisateur non trouvé"})
                return
            corps = self._lire_corps()
            DB["utilisateurs"][uid].update(corps)
            self._envoyer_reponse(200, DB["utilisateurs"][uid])
        else:
            self._envoyer_reponse(404, {"erreur": "Route non trouvée"})

    def do_DELETE(self):
        if self.path.startswith("/api/v1/utilisateurs/"):
            try:
                uid = int(self.path.split("/")[-1])
            except ValueError:
                self._envoyer_reponse(400, {"erreur": "ID invalide"})
                return
            if uid not in DB["utilisateurs"]:
                self._envoyer_reponse(404, {"erreur": "Utilisateur non trouvé"})
                return
            del DB["utilisateurs"][uid]
            self.send_response(204)
            self.end_headers()
        else:
            self._envoyer_reponse(404, {"erreur": "Route non trouvée"})

# Démarrage du serveur
serveur = HTTPServer(("localhost", 8888), RESTHandler)
thread = threading.Thread(target=serveur.serve_forever, daemon=True)
thread.start()
print("Serveur REST démarré sur http://localhost:8888")
time.sleep(0.1)

def appel_api(methode: str, chemin: str, corps: dict = None) -> tuple:
    """Client HTTP minimal pour tester l'API."""
    url = f"http://localhost:8888{chemin}"
    data = json.dumps(corps).encode() if corps else None
    headers = {"Content-Type": "application/json"} if corps else {}
    req = urllib.request.Request(url, data=data, headers=headers, method=methode)
    try:
        with urllib.request.urlopen(req) as resp:
            return resp.status, json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        return e.code, json.loads(e.read().decode())

# Tests de l'API
print("\n--- Tests de l'API REST ---")

# GET liste
code, data = appel_api("GET", "/api/v1/utilisateurs")
print(f"\nGET /utilisateurs → {code}")
print(f"  Total : {data['pagination']['total']} utilisateurs")

# GET détail avec HATEOAS
code, data = appel_api("GET", "/api/v1/utilisateurs/1")
print(f"\nGET /utilisateurs/1 → {code}")
print(f"  {data['nom']} ({data['email']})")
print(f"  Liens HATEOAS : {list(data['_links'].keys())}")

# POST créer
code, data = appel_api("POST", "/api/v1/utilisateurs",
                        {"nom": "Damien Leclerc", "email": "damien@example.com"})
print(f"\nPOST /utilisateurs → {code} (créé id={data['id']})")

# POST doublon email
code, data = appel_api("POST", "/api/v1/utilisateurs",
                        {"nom": "Autre", "email": "alice@example.com"})
print(f"\nPOST /utilisateurs (doublon email) → {code}: {data['erreur']}")

# PATCH mise à jour partielle
code, data = appel_api("PATCH", "/api/v1/utilisateurs/2", {"role": "moderateur"})
print(f"\nPATCH /utilisateurs/2 → {code}: role={data['role']}")

# DELETE
code, _ = appel_api("DELETE", "/api/v1/utilisateurs/3"), ""
print(f"\nDELETE /utilisateurs/3 → {code[0]}")

# GET 404
code, data = appel_api("GET", "/api/v1/utilisateurs/999")
print(f"\nGET /utilisateurs/999 → {code}: {data['erreur']}")

serveur.shutdown()
```

## Visualisation des bonnes pratiques REST

```{code-cell} python
:tags: [hide-input]

# Scorecard visuel : bonnes pratiques API REST
fig, ax = plt.subplots(figsize=(12, 7))
ax.set_xlim(0, 12)
ax.set_ylim(-0.5, 12)
ax.axis("off")
ax.set_title("Checklist des bonnes pratiques API REST", fontsize=13, fontweight="bold")

bonnes_pratiques = [
    ("NOMMAGE", [
        ("Utiliser des noms de ressources au pluriel (/articles, /utilisateurs)", True),
        ("Utiliser des minuscules et tirets (/articles-de-blog)", True),
        ("Hiérarchie pour les sous-ressources (/users/42/orders)", True),
        ("Éviter les verbes dans les URIs (/getUser, /deleteArticle)", False),
    ]),
    ("MÉTHODES", [
        ("GET pour la lecture, POST pour la création", True),
        ("PUT pour le remplacement complet, PATCH pour la mise à jour partielle", True),
        ("DELETE retourne 204 No Content ou 200 OK", True),
        ("GET modifie l'état du serveur (side effects)", False),
    ]),
    ("RÉPONSES", [
        ("Utiliser les codes HTTP appropriés (201 Created, 204 No Content)", True),
        ("Inclure un corps JSON d'erreur descriptif pour 4xx/5xx", True),
        ("Paginer les collections volumineuses", True),
        ("Retourner 200 OK pour tout (même les erreurs)", False),
    ]),
    ("SÉCURITÉ", [
        ("Authentification par JWT Bearer token", True),
        ("HTTPS obligatoire pour tout le trafic", True),
        ("Validation et sanitisation de toutes les entrées", True),
        ("Exposer des IDs internes séquentiels prévisibles", False),
    ]),
]

y = 11
for categorie, pratiques in bonnes_pratiques:
    ax.text(0.3, y, categorie, fontsize=10, fontweight="bold", color="#37474F")
    y -= 0.5
    for pratique, bon in pratiques:
        couleur = "#2E7D32" if bon else "#C62828"
        signe = "✓" if bon else "✗"
        style = "normal" if bon else "italic"
        ax.text(0.6, y, f"{signe}  {pratique}", fontsize=8.5, color=couleur,
                style=style, va="top")
        y -= 0.7
    y -= 0.2

plt.tight_layout()
plt.show()
```

## Résumé

REST est avant tout un **style architectural** fondé sur des contraintes qui produisent des systèmes évolutifs et interopérables. Les APIs REST modernes sont construites autour de quelques principes simples : des URIs identifiant des ressources (noms, pas verbes), des méthodes HTTP avec leur sémantique correcte (idempotence, sécurité), des codes de statut appropriés, et une authentification par token.

La spécification OpenAPI permet de documenter, valider et générer du code client/serveur automatiquement. HATEOAS, bien que rarement implémenté en totalité, guide vers des APIs auto-descriptives où le client découvre les actions disponibles à partir des réponses du serveur.
