Cours Python · Module 4: Données, API et bases de données

API REST - l'architecture d'échange de données

12 min de lecture
Dans cette leçon11

Content de te revoir ! Ici Darwin, avec une nouvelle leçon sur le flux de données.

Tu as appris les formats de données (JSON, CSV, XML) et le protocole HTTP (GET, POST, PUT, DELETE). Il est maintenant temps de parler des API REST - l'architecture standard pour construire des API, utilisée dans le monde entier !

Analogie safari : HTTP est le système de messagerie. Une API REST, ce sont les procédures standard - les règles selon lesquelles tu organises les stations de recherche, les catalogues de données et la communication entre eux. Grâce à ces règles, n'importe quel biologiste, quel que soit son pays d'origine, sait comment récupérer les données du lion : GET /api/species/lion

Qu'est-ce qu'une API REST ?

REST (REpresentational State Transfer) est un style architectural pour les API - un ensemble de règles et de conventions pour concevoir des API qui utilisent HTTP.

Une API (Application Programming Interface) est une interface de programmation - la façon dont les applications communiquent entre elles.

API REST = API HTTP conçue selon les principes REST

Pourquoi REST ?

Sans standards, chaque API aurait une allure différente :

  • /getLion?id=123
  • /fetchLionData
  • /lion-information/get
  • /api/v1/animals/lion/retrieve

Avec REST, tout le monde sait :

  • GET /api/species/lion - récupérer un lion
  • POST /api/species - créer une espèce
  • DELETE /api/species/lion - supprimer un lion

Prévisible, cohérent, intuitif !

Les principes REST

1. Les ressources - des noms, pas des verbes

En REST, tout est une ressource - un objet, une entité ou une donnée.

Bien - des noms :

  • /species - espèces
  • /observations - observations
  • /habitats - habitats
  • /researchers - chercheurs

Mal - des verbes :

  • /getSpecies
  • /createObservation
  • /deleteHabitat

L'action est déterminée par la méthode HTTP, pas par l'URL !

2. Les méthodes HTTP = les actions sur les ressources

MéthodeActionExemple
GETRécupérer la/les ressource(s)GET /species - liste, GET /species/lion - une seule
POSTCréer une nouvelle ressourcePOST /species - ajouter une espèce
PUTRemplacer la ressource entièrePUT /species/lion - remplacer les données du lion
PATCHMettre à jour une partie de la ressourcePATCH /species/lion - changer la population
DELETESupprimer la ressourceDELETE /species/lion - supprimer le lion

3. Sans état (stateless)

Chaque requête doit contenir toutes les informations nécessaires à son traitement. Le serveur ne mémorise pas les requêtes précédentes.

Exemple :

1# Chaque requête contient le token d'autorisation
2GET /api/species/lion
3Authorization: Bearer abc123
4
5GET /api/observations
6Authorization: Bearer abc123

Le serveur ne sait pas « qui a envoyé ça plus tôt » - chaque requête est autonome !

4. La hiérarchie des ressources

Les ressources peuvent être imbriquées hiérarchiquement :

1/species/lion                    # Espèce : lion
2/species/lion/observations       # Observations du lion
3/species/lion/observations/123   # Observation #123 spécifique du lion
4
5/habitats/serengeti              # Habitat : Serengeti
6/habitats/serengeti/species      # Espèces du Serengeti

Règle : /resource/id/subresource/id

5. Le versionnage de l'API

Les API évoluent - le versionnage évite de casser les clients existants :

1/api/v1/species
2/api/v2/species

Change de version quand :

  • Tu retires des champs des réponses
  • Tu changes le format des données
  • Tu supprimes des endpoints

Les endpoints RESTful - les schémas standard

CRUD (Create, Read, Update, Delete) pour une ressource

1# LIST - lister toutes les espèces
2GET /api/species
3Response: [{"id": "lion", "name": "Lion"}, {"id": "elephant", "name": "Elephant"}]
4
5# GET - une espèce
6GET /api/species/lion
7Response: {"id": "lion", "scientific_name": "Panthera leo", "population": 120}
8
9# CREATE - créer une nouvelle espèce
10POST /api/species
11Body: {"scientific_name": "Gorilla gorilla", "population": 230}
12Response: {"id": "gorilla", "scientific_name": "Gorilla gorilla", "population": 230}
13
14# UPDATE (complet) - remplacer l'espèce
15PUT /api/species/lion
16Body: {"scientific_name": "Panthera leo", "population": 125, "habitat": "savanna"}
17Response: {"id": "lion", ...}
18
19# UPDATE (partiel) - changer seulement la population
20PATCH /api/species/lion
21Body: {"population": 125}
22Response: {"id": "lion", "population": 125, ...}
23
24# DELETE - supprimer l'espèce
25DELETE /api/species/lion
26Response: 204 No Content (ou 200 avec confirmation)

Ressources imbriquées

1# Lister les observations d'un lion
2GET /api/species/lion/observations
3Response: [
4  {"id": "obs1", "date": "2024-01-15", "count": 12},
5  {"id": "obs2", "date": "2024-01-20", "count": 8}
6]
7
8# Observation spécifique d'un lion
9GET /api/species/lion/observations/obs1
10Response: {"id": "obs1", "species": "lion", "date": "2024-01-15", "count": 12}
11
12# Ajouter une observation pour un lion
13POST /api/species/lion/observations
14Body: {"date": "2024-01-25", "count": 10, "location": "Serengeti"}
15Response: {"id": "obs3", "species": "lion", ...}

Les query parameters - filtrage, tri, pagination

Filtrage

1# Espèces menacées
2GET /api/species?endangered=true
3
4# Espèces de la savane avec une population > 100
5GET /api/species?habitat=savanna&min_population=100
6
7# Observations dans une plage de dates
8GET /api/observations?start_date=2024-01-01&end_date=2024-01-31

Tri

1# Trier par population croissante
2GET /api/species?sort=population
3
4# Trier par population décroissante
5GET /api/species?sort=-population
6
7# Trier par nom, puis par population
8GET /api/species?sort=name,population

Pagination

1# Page 1, 20 résultats par page
2GET /api/species?page=1&per_page=20
3
4# Alternative : offset et limit
5GET /api/species?offset=0&limit=20
6
7# Réponse avec les métadonnées de pagination :
8{
9  "data": [...],
10  "pagination": {
11    "current_page": 1,
12    "per_page": 20,
13    "total_items": 150,
14    "total_pages": 8
15  }
16}

Sélection de champs

1# Seulement le nom et la population
2GET /api/species?fields=name,population
3
4Response: [
5  {"name": "Lion", "population": 120},
6  {"name": "Elephant", "population": 450}
7]

La structure d'une réponse JSON

Succès - ressource unique

1{
2  "id": "lion",
3  "scientific_name": "Panthera leo",
4  "common_name": "Lion",
5  "population": 120,
6  "habitat": "savanna",
7  "endangered": true,
8  "created_at": "2024-01-01T10:00:00Z",
9  "updated_at": "2024-01-15T14:30:00Z"
10}

Succès - liste de ressources avec pagination

1{
2  "data": [
3    {"id": "lion", "name": "Lion", "population": 120},
4    {"id": "elephant", "name": "Elephant", "population": 450}
5  ],
6  "pagination": {
7    "page": 1,
8    "per_page": 20,
9    "total": 45,
10    "pages": 3
11  },
12  "links": {
13    "self": "/api/species?page=1",
14    "next": "/api/species?page=2",
15    "last": "/api/species?page=3"
16  }
17}

Erreur - format standard

1{
2  "error": {
3    "code": "SPECIES_NOT_FOUND",
4    "message": "L'espèce 'unicorn' n'existe pas",
5    "status": 404,
6    "timestamp": "2024-01-15T14:30:00Z"
7  }
8}

Erreurs de validation

1{
2  "error": {
3    "code": "VALIDATION_ERROR",
4    "message": "Erreurs de validation des données",
5    "status": 400,
6    "details": [
7      {"field": "population", "message": "Doit être un nombre supérieur à 0"},
8      {"field": "habitat", "message": "Champ requis"}
9    ]
10  }
11}

Les codes de statut HTTP en REST

2xx - Succès

  • 200 OK - GET, PUT, PATCH - succès avec données
  • 201 Created - POST - ressource créée (+ en-tête Location)
  • 204 No Content - DELETE - succès sans données

4xx - Erreur client

  • 400 Bad Request - données invalides (par ex. mauvais JSON, validation)
  • 401 Unauthorized - pas d'authentification
  • 403 Forbidden - pas de permission (authentifié, mais sans accès)
  • 404 Not Found - la ressource n'existe pas
  • 409 Conflict - conflit (par ex. ID en double)
  • 422 Unprocessable Entity - format valide, mais données logiquement incorrectes
  • 429 Too Many Requests - limite de débit dépassée

5xx - Erreur serveur

  • 500 Internal Server Error - erreur du serveur
  • 503 Service Unavailable - service indisponible

Authentification et autorisation

Clé d'API

1GET /api/species
2X-API-Key: your_api_key_here

Bearer Token (JWT)

1GET /api/species
2Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Basic Auth

1GET /api/species
2Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

Le plus courant : Bearer Token (OAuth 2.0, JWT)

Exemple safari - client d'API REST complet

1import requests
2from typing import List, Dict, Optional, Any
3from datetime import datetime
4
5class SafariRESTClient:
6    """Client d'API REST pour Safari Data API"""
7
8    def __init__(self, base_url: str, api_key: str):
9        self.base_url = base_url.rstrip("/")
10        self.session = requests.Session()
11        self.session.headers.update({
12            "Authorization": f"Bearer {api_key}",
13            "Accept": "application/json",
14            "Content-Type": "application/json"
15        })
16
17    # === SPECIES - /api/species ===
18
19    def list_species(self, habitat: Optional[str] = None,
20                    endangered: Optional[bool] = None,
21                    page: int = 1, per_page: int = 20,
22                    sort: str = "name") -> Dict[str, Any]:
23        """
24        GET /api/species
25        Liste les espèces avec filtrage, tri, pagination
26        """
27        params = {
28            "page": page,
29            "per_page": per_page,
30            "sort": sort
31        }
32        if habitat:
33            params["habitat"] = habitat
34        if endangered is not None:
35            params["endangered"] = str(endangered).lower()
36
37        response = self.session.get(f"{self.base_url}/api/species", params=params)
38        response.raise_for_status()
39        return response.json()
40
41    def get_species(self, species_id: str) -> Dict[str, Any]:
42        """
43        GET /api/species/{id}
44        Récupère une seule espèce
45        """
46        response = self.session.get(f"{self.base_url}/api/species/{species_id}")
47        response.raise_for_status()
48        return response.json()
49
50    def create_species(self, data: Dict[str, Any]) -> Dict[str, Any]:
51        """
52        POST /api/species
53        Crée une nouvelle espèce
54        """
55        response = self.session.post(f"{self.base_url}/api/species", json=data)
56        response.raise_for_status()
57
58        if response.status_code == 201:
59            # Renvoie la ressource créée + l'ID depuis l'en-tête Location
60            location = response.headers.get("Location")
61            print(f"Créé : {location}")
62
63        return response.json()
64
65    def update_species(self, species_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
66        """
67        PUT /api/species/{id}
68        Remplace l'espèce entière
69        """
70        response = self.session.put(
71            f"{self.base_url}/api/species/{species_id}",
72            json=data
73        )
74        response.raise_for_status()
75        return response.json()
76
77    def partial_update_species(self, species_id: str,
78                              data: Dict[str, Any]) -> Dict[str, Any]:
79        """
80        PATCH /api/species/{id}
81        Met à jour une partie d'une espèce
82        """
83        response = self.session.patch(
84            f"{self.base_url}/api/species/{species_id}",
85            json=data
86        )
87        response.raise_for_status()
88        return response.json()
89
90    def delete_species(self, species_id: str) -> bool:
91        """
92        DELETE /api/species/{id}
93        Supprime une espèce
94        """
95        response = self.session.delete(f"{self.base_url}/api/species/{species_id}")
96        response.raise_for_status()
97        return response.status_code == 204
98
99    # === OBSERVATIONS - /api/species/{id}/observations ===
100
101    def list_observations(self, species_id: str,
102                         start_date: Optional[str] = None,
103                         end_date: Optional[str] = None,
104                         page: int = 1) -> Dict[str, Any]:
105        """
106        GET /api/species/{id}/observations
107        Liste les observations d'une espèce
108        """
109        params = {"page": page}
110        if start_date:
111            params["start_date"] = start_date
112        if end_date:
113            params["end_date"] = end_date
114
115        response = self.session.get(
116            f"{self.base_url}/api/species/{species_id}/observations",
117            params=params
118        )
119        response.raise_for_status()
120        return response.json()
121
122    def create_observation(self, species_id: str,
123                          data: Dict[str, Any]) -> Dict[str, Any]:
124        """
125        POST /api/species/{id}/observations
126        Ajoute une observation pour une espèce
127        """
128        response = self.session.post(
129            f"{self.base_url}/api/species/{species_id}/observations",
130            json=data
131        )
132        response.raise_for_status()
133        return response.json()
134
135    def get_observation(self, species_id: str, observation_id: str) -> Dict[str, Any]:
136        """
137        GET /api/species/{id}/observations/{obs_id}
138        Récupère une observation spécifique
139        """
140        response = self.session.get(
141            f"{self.base_url}/api/species/{species_id}/observations/{observation_id}"
142        )
143        response.raise_for_status()
144        return response.json()
145
146    # === STATISTICS - /api/statistics ===
147
148    def get_statistics(self, group_by: str = "habitat") -> Dict[str, Any]:
149        """
150        GET /api/statistics
151        Récupère les statistiques
152        """
153        params = {"group_by": group_by}
154        response = self.session.get(
155            f"{self.base_url}/api/statistics",
156            params=params
157        )
158        response.raise_for_status()
159        return response.json()
160
161    def close(self):
162        """Fermer la session"""
163        self.session.close()
164
165
166# === DÉMONSTRATION ===
167
168print("=== CLIENT D'API REST SAFARI ===\n")
169
170client = SafariRESTClient(
171    base_url="https://api.safari-data.org",
172    api_key="demo_bearer_token_12345"
173)
174
175# 1. LIST - récupérer toutes les espèces menacées de la savane
176print("1. Liste des espèces (filtrée, triée, paginée)")
177result = client.list_species(
178    habitat="savanna",
179    endangered=True,
180    page=1,
181    per_page=10,
182    sort="-population"  # Décroissant par population
183)
184
185print(f"   Trouvées : {result['pagination']['total']} espèces")
186for species in result['data']:
187    print(f"   - {species['common_name']} : {species['population']} individus")
188
189# 2. GET - récupérer une espèce spécifique
190print("\n2. Récupération d'une espèce spécifique")
191lion = client.get_species("lion")
192print(f"   {lion['common_name']} ({lion['scientific_name']})")
193print(f"   Population : {lion['population']}")
194print(f"   Menacée : {'Oui' if lion['endangered'] else 'Non'}")
195
196# 3. POST - créer une nouvelle espèce
197print("\n3. Création d'une nouvelle espèce")
198new_species = {
199    "scientific_name": "Acinonyx jubatus",
200    "common_name": "Cheetah",
201    "population": 7100,
202    "habitat": "savanna",
203    "endangered": True
204}
205
206created = client.create_species(new_species)
207print(f"   Créée : {created['common_name']} (ID : {created['id']})")
208
209# 4. PATCH - mettre à jour seulement la population
210print("\n4. Mise à jour partielle (PATCH)")
211updated = client.partial_update_species("lion", {"population": 125})
212print(f"   Population mise à jour : {updated['population']}")
213
214# 5. Observations imbriquées - liste
215print("\n5. Liste des observations du lion")
216observations = client.list_observations(
217    "lion",
218    start_date="2024-01-01",
219    end_date="2024-01-31"
220)
221
222print(f"   {len(observations['data'])} observations trouvées en janvier")
223for obs in observations['data']:
224    print(f"   - {obs['date']} : {obs['count']} individus à {obs['location']}")
225
226# 6. Observations imbriquées - création
227print("\n6. Ajout d'une nouvelle observation")
228new_obs = {
229    "date": datetime.now().isoformat(),
230    "location": "Serengeti North",
231    "count": 15,
232    "notes": "Groupe avec 3 lionceaux"
233}
234
235obs_created = client.create_observation("lion", new_obs)
236print(f"   ID de l'observation créée : {obs_created['id']}")
237
238# 7. Statistiques
239print("\n7. Récupération des statistiques")
240stats = client.get_statistics(group_by="habitat")
241print("   Statistiques par habitat :")
242for habitat_stat in stats['data']:
243    print(f"   - {habitat_stat['habitat']} : {habitat_stat['species_count']} espèces")
244
245# 8. DELETE - supprimer une espèce
246print("\n8. Suppression d'une espèce")
247if client.delete_species("test-species"):
248    print("   Espèce supprimée")
249
250client.close()
251print("\nDémonstration terminée")

Bonnes pratiques des API REST

1. Utilise des noms au pluriel

Bien : /api/species, /api/observations Mal : /api/specie, /api/observation

2. Versionne toujours ton API

Bien : /api/v1/species Mal : /api/species (que faire quand tu changes le format ?)

3. Hiérarchie de ressources jamais plus profonde que 2 niveaux

Bien : /species/lion/observations Mal : /continents/africa/countries/kenya/parks/serengeti/species/lion/observations

4. Utilise snake_case ou camelCase - de façon cohérente

Bien : {"scientific_name": "..."} ou {"scientificName": "..."} Mal : {"scientific_name": "...", "commonName": "..."} - mélangé !

5. Filtres, tri, pagination via les query params

Bien : GET /species?habitat=savanna&sort=-population&page=2 Mal : POST /species/filter avec un corps

6. Renvoie les codes de statut appropriés

  • 201 Created (et non 200) pour POST
  • 204 No Content pour DELETE
  • 404 Not Found quand la ressource n'existe pas
  • 400 Bad Request pour les erreurs de validation

7. Utilise l'en-tête Location pour les nouvelles ressources

1POST /api/species
2Response: 201 Created
3Location: /api/species/cheetah

8. Rate limiting - communique les limites

1Response Headers:
2X-RateLimit-Limit: 1000
3X-RateLimit-Remaining: 999
4X-RateLimit-Reset: 1640000000

Résumé

Dans cette leçon, tu as appris :

  • Ce qu'est une API REST et à quoi elle sert
  • Les principes REST : ressources, méthodes HTTP, sans état, hiérarchie
  • Les endpoints RESTful : /api/v1/resources/:id
  • Les opérations CRUD : GET, POST, PUT, PATCH, DELETE
  • Les query parameters : filtrage, tri, pagination
  • La structure d'une réponse JSON (data, pagination, error)
  • Les codes de statut dans le contexte REST
  • L'authentification : clé d'API, Bearer Token
  • Les bonnes pratiques des API REST

Point de contrôle

Avant de continuer :

  • Tu comprends la différence entre POST /species et GET /species/{id}
  • Tu sais concevoir un endpoint RESTful
  • Tu sais utiliser les query params pour filtrer
  • Tu comprends les ressources imbriquées (/species/{id}/observations)
  • Tu connais les bonnes pratiques des API REST

Analogie safari : Une API REST est comme des standards de communication universels entre les stations de recherche du monde entier. Grâce à eux, un biologiste de Pologne sait que GET /api/species/lion renverra les données du lion - quel que soit le concepteur de l'API !

Dans la prochaine leçon, Darwin t'enseignera le web scraping - comment collecter des données depuis des sites web qui n'ont pas d'API !

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. REST signifie :

  2. 2. Quelle méthode HTTP correspond à l'opération UPDATE du CRUD ?

Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Envoie un POST avec les données animal = {'name': 'Tygrys'} vers l'URL '/api/animals'

  • Éditeur de code

    Envoie un GET avec l'en-tête Authorization: Bearer token123

  • Tri vertical

    Remets dans l'ordre les étapes d'une requête HTTP :

  • Tri horizontal

    Remets les éléments dans le bon ordre :

Articles utiles