Chapitre 827 minIntermédiaire
Consommer une API : fetch en JavaScript, PHP, Python et C#
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- appeler une API avec fetch et traiter correctement ses erreurs ;
- faire le même appel en PHP, en Python ou en C# ;
- fixer un délai maximal et réessayer sans danger ;
- garder les secrets hors du code des clients.
5 exercices corrigés
Consommer une API, c'est écrire le client : le code qui construit la requête HTTP, l'envoie, attend la réponse, vérifie son code de statut et transforme le JSON reçu en données du programme. Ce chapitre montre comment le faire proprement en JavaScript avec fetch, en PHP avec cURL, en Python avec requests et en C# avec HttpClient, puis les règles communes : délais d'attente, gestion des erreurs, nouvelles tentatives et protection des secrets.
Ce que tout client doit faire
Quel que soit le langage, un appel d'API robuste suit les mêmes étapes :
- Construire l'URL en encodant les paramètres (jamais par simple concaténation d'une saisie).
- Ajouter les en-têtes :
Accept,Authorization, etContent-Types'il y a un corps. - Sérialiser le corps en JSON.
- Fixer un délai maximal (timeout) : sans lui, un serveur qui ne répond pas bloque ton programme indéfiniment.
- Distinguer deux sortes d'échecs : l'échec réseau (serveur injoignable, délai dépassé, certificat invalide), où il n'y a pas de réponse du tout, et l'échec HTTP (
4xx,5xx), où il y a une réponse qu'il faut lire. - Lire le corps selon son type : JSON pour une réussite,
application/problem+jsonpour une erreur, rien pour un204. - Réagir au code : afficher l'erreur de validation à côté du champ (
422), renvoyer vers la connexion (401), réessayer plus tard (429,503), ne jamais réessayer une erreur400.
Dans les exemples, l'API est celle d'AppliFrais à l'adresse https://api.gsb.example/v1 ; chacun a été testé contre l'API de démonstration.
JavaScript : fetch, les bases
fetch est la fonction standard des navigateurs pour faire des requêtes HTTP, disponible aussi dans Node.js depuis la version 18. Elle renvoie une promesse de réponse ; avec async et await, le code se lit de haut en bas (rappels sur l'asynchrone en JavaScript).
const API = 'https://api.gsb.example/v1';
async function chargerFiche(id, jeton) {
const reponse = await fetch(`${API}/fiches/${id}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${jeton}`,
},
});
if (!reponse.ok) {
throw new Error(`Erreur ${reponse.status}`);
}
return reponse.json(); // une autre promesse : le corps lu et converti
}
const fiche = await chargerFiche(12, jeton);
console.log(fiche.mois, fiche.etat); // 2026-10 CR
Deux await sont nécessaires : le premier attend les en-têtes de la réponse, le second (reponse.json()) attend et convertit le corps.
L'objet Response donne tout ce qu'il faut :
| Propriété ou méthode | Contenu |
|---|---|
reponse.ok | true si le code est entre 200 et 299 |
reponse.status | Le code, par exemple 404 |
reponse.headers.get('Location') | Un en-tête (le nom est insensible à la casse) |
await reponse.json() | Le corps converti depuis JSON ; lève une erreur si le corps n'est pas du JSON |
await reponse.text() | Le corps brut, en texte |
await reponse.blob() | Le corps en binaire (un PDF, une image) |
Le corps ne se lit qu'une seule fois : appeler reponse.json() après reponse.text() lève une erreur.
JavaScript : envoyer des données et paramétrer la requête
Pour un POST, un PUT ou un PATCH, on précise la méthode, l'en-tête Content-Type et le corps sérialisé :
const reponse = await fetch(`${API}/fiches/12/frais-hors-forfait`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jeton}`,
},
body: JSON.stringify({ date: '2026-10-14', libelle: 'Repas client à Lyon', montant: 23.5 }),
});
if (reponse.status === 201) {
console.log('Créé à', reponse.headers.get('Location'));
}
Oublier JSON.stringify envoie la chaîne "[object Object]" ; oublier Content-Type fait souvent refuser le corps par le serveur (415) ou le laisse vide côté Express.
Pour les paramètres de la chaîne de requête, URLSearchParams encode tout correctement, accents et espaces compris :
const params = new URLSearchParams({ etat: 'CL', mois: '2026-10', q: 'note de frais' });
const url = `${API}/fiches?${params}`;
// https://api.gsb.example/v1/fiches?etat=CL&mois=2026-10&q=note+de+frais
URLSearchParams code l'espace en +, la convention des formulaires HTML, au lieu de %20 : les deux formes sont comprises par les serveurs dans la chaîne de requête.
Et pour fixer un délai maximal, AbortSignal.timeout annule la requête au bout du temps donné :
try {
const reponse = await fetch(`${API}/fiches`, { signal: AbortSignal.timeout(5000) });
// …
} catch (e) {
if (e.name === 'TimeoutError') console.error("L'API n'a pas répondu en 5 secondes");
else throw e;
}
JavaScript : une fonction d'appel réutilisable
Dans un vrai projet, on ne répète pas ces quinze lignes à chaque appel. On écrit une fonction qui ajoute le jeton, sérialise le corps, fixe le délai et transforme toute réponse d'erreur en exception exploitable :
class ErreurApi extends Error {
constructor(probleme) {
super(probleme.detail ?? probleme.title);
this.status = probleme.status;
this.type = probleme.type;
this.probleme = probleme; // le corps RFC 9457 complet, avec ses extensions
}
}
async function appelerApi(chemin, { methode = 'GET', corps, signal } = {}) {
const reponse = await fetch(`${API}${chemin}`, {
method: methode,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${lireJeton()}`,
...(corps !== undefined && { 'Content-Type': 'application/json' }),
},
body: corps !== undefined ? JSON.stringify(corps) : undefined,
signal: signal ?? AbortSignal.timeout(5000),
});
if (reponse.status === 204) return null;
const type = reponse.headers.get('Content-Type') ?? '';
const donnees = type.includes('json') ? await reponse.json() : await reponse.text();
if (!reponse.ok) {
throw new ErreurApi(typeof donnees === 'object' ? donnees : { status: reponse.status, title: donnees });
}
return donnees;
}
Le code appelant devient court, et chaque erreur se traite selon son code :
try {
const fiche = await appelerApi('/fiches', { methode: 'POST', corps: { mois: moisChoisi } });
afficherFiche(fiche);
} catch (e) {
if (!(e instanceof ErreurApi)) afficherMessage('Connexion impossible. Vérifie ton réseau.');
else if (e.status === 401) redirigerVersConnexion();
else if (e.status === 409) afficherMessage('Tu as déjà une fiche pour ce mois.');
else if (e.status === 422) afficherErreursChamps(e.probleme.erreurs);
else afficherMessage(`Erreur inattendue (${e.status}). Réessaie plus tard.`);
}
Dans un composant React, l'appel se place dans un gestionnaire d'événement ou dans un effet, avec un état pour le chargement et l'erreur (appels API avec React). Des bibliothèques comme Axios offrent les mêmes services que cette fonction ; fetch suffit largement pour un projet de BTS. Une application mobile React Native fait exactement le même appel, avec en plus le jeton gardé dans un stockage sécurisé et le cas du réseau absent (appeler une API depuis une application mobile).
PHP : cURL
Une application PHP appelle une API quand elle consomme un service extérieur : vérifier un numéro SIRET, géocoder une adresse, transmettre une commande à un partenaire. L'extension cURL est la solution standard :
<?php
function appelerApi(string $methode, string $url, ?array $corps = null, string $jeton = ''): array
{
$ch = curl_init($url);
$entetes = ['Accept: application/json'];
if ($jeton !== '') {
$entetes[] = 'Authorization: Bearer ' . $jeton;
}
if ($corps !== null) {
$entetes[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($corps, JSON_THROW_ON_ERROR));
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $methode,
CURLOPT_HTTPHEADER => $entetes,
CURLOPT_RETURNTRANSFER => true, // renvoyer le corps au lieu de l'afficher
CURLOPT_CONNECTTIMEOUT => 3, // secondes pour établir la connexion
CURLOPT_TIMEOUT => 10, // secondes au total
]);
$texte = curl_exec($ch);
if ($texte === false) {
throw new RuntimeException('Erreur réseau : ' . curl_error($ch));
}
$statut = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$donnees = $texte === '' ? null : json_decode($texte, true, 512, JSON_THROW_ON_ERROR);
return ['statut' => $statut, 'donnees' => $donnees];
}
$reponse = appelerApi('GET', 'https://geo.api.gouv.fr/communes?' . http_build_query([
'codePostal' => '69001',
'fields' => 'nom,population',
]));
if ($reponse['statut'] === 200) {
foreach ($reponse['donnees'] as $commune) {
echo htmlspecialchars($commune['nom']), "\n";
}
}
Quelques points à retenir :
CURLOPT_RETURNTRANSFERest indispensable : sans lui,curl_execaffiche le corps au lieu de le renvoyer ;curl_execrenvoiefalseseulement en cas d'échec réseau ; un404ou un500est une réponse normale, qu'il faut détecter avecCURLINFO_RESPONSE_CODE, comme avecfetch;http_build_query()construit et encode la chaîne de requête ;- les données reçues d'une API tierce sont des entrées non fiables : on les échappe à l'affichage (
htmlspecialchars) et on les valide avant de les enregistrer, comme une saisie d'utilisateur ; curl_close()n'a plus d'effet depuis PHP 8.0 : la ressource est libérée automatiquement.
Pour une application plus importante, les frameworks fournissent un client de plus haut niveau : HttpClient de Symfony, le client Http de Laravel, ou la bibliothèque Guzzle. Ils gèrent les délais, les nouvelles tentatives et le JSON avec moins de code.
Python : requests
En Python, la bibliothèque requests reste la plus utilisée (pip install requests) ; httpx, très proche, ajoute l'asynchrone et HTTP/2.
import requests
API = "https://api.gsb.example/v1"
session = requests.Session() # réutilise la connexion et les en-têtes communs
session.headers.update({"Authorization": f"Bearer {jeton}", "Accept": "application/json"})
# Lire une liste filtrée : params encode la chaîne de requête
reponse = session.get(f"{API}/fiches", params={"etat": "CR", "limite": 20}, timeout=5)
reponse.raise_for_status() # lève HTTPError pour un code 4xx ou 5xx
for fiche in reponse.json()["donnees"]:
print(fiche["id"], fiche["mois"], fiche["etat"])
# Créer une fiche et traiter les erreurs
try:
reponse = session.post(f"{API}/fiches", json={"mois": "2026-13"}, timeout=5)
reponse.raise_for_status()
print("Créée :", reponse.headers["Location"])
except requests.HTTPError as e:
probleme = e.response.json()
print(e.response.status_code, probleme["title"])
for erreur in probleme.get("erreurs", []):
print(" -", erreur["champ"], ":", erreur["message"])
except requests.RequestException as e:
print("Problème réseau :", e)
Ce script affiche, contre l'API de démonstration :
12 2026-10 CR
13 2026-11 CR
422 Données invalides
- mois : Format attendu : AAAA-MM
Le paramètre json= sérialise le corps et ajoute Content-Type: application/json ; params= encode la chaîne de requête. Attention : requests n'a aucun délai par défaut. Sans timeout=, un appel peut attendre indéfiniment, ce qui bloque un script planifié pendant des heures.
C# : HttpClient
En C#, la classe HttpClient et les méthodes d'extension de System.Net.Http.Json convertissent directement entre JSON et objets. C'est ce qu'utilise une application WinForms ou WPF qui affiche les données d'une API au lieu de se connecter à la base (consommer une API REST en C#).
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using var http = new HttpClient
{
BaseAddress = new Uri("https://api.gsb.example/v1/"),
Timeout = TimeSpan.FromSeconds(5),
};
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jeton);
// Lire une fiche : le JSON devient un objet Fiche
Fiche? fiche = await http.GetFromJsonAsync<Fiche>("fiches/12");
Console.WriteLine($"{fiche!.Mois} {fiche.Etat}"); // 2026-10 CR
// Créer une fiche et lire le code de statut
HttpResponseMessage reponse = await http.PostAsJsonAsync("fiches", new { mois = "2026-11" });
if (reponse.StatusCode == HttpStatusCode.Created)
{
Fiche? creee = await reponse.Content.ReadFromJsonAsync<Fiche>();
Console.WriteLine($"Créée : {reponse.Headers.Location} (id {creee!.Id})");
}
else
{
Probleme? p = await reponse.Content.ReadFromJsonAsync<Probleme>();
Console.WriteLine($"{(int)reponse.StatusCode} {p?.Title} : {p?.Detail}");
}
record Fiche(int Id, int VisiteurId, string Mois, string Etat, decimal? MontantValide);
record Probleme(string Type, string Title, int Status, string? Detail);
Les méthodes …FromJsonAsync et …AsJsonAsync utilisent les réglages « web » de System.Text.Json : les noms en camelCase du JSON (montantValide) correspondent aux propriétés C# (MontantValide). Le type decimal? garde le montant exact et accepte null.
Réessayer intelligemment
Certaines erreurs sont passagères : 429 Too Many Requests, 503 Service Unavailable, 502, 504, un délai dépassé. Réessayer a du sens, à trois conditions :
- La requête est idempotente (
GET,PUT,DELETE), ou protégée par une clé d'idempotence (chapitre 6). On ne rejoue jamais unPOSTordinaire en aveugle. - On respecte
Retry-Afterquand le serveur le donne : il dit combien de secondes attendre (ou une date). - On espace les essais de plus en plus (attente exponentielle : 1 s, 2 s, 4 s…), avec un peu de hasard (jitter) pour que mille clients ne reviennent pas tous à la même seconde, et on abandonne après quelques essais.
const attendre = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const CODES_PASSAGERS = [429, 502, 503, 504];
const METHODES_IDEMPOTENTES = ['GET', 'HEAD', 'PUT', 'DELETE'];
async function fetchAvecReessais(url, options = {}, essaisMax = 4) {
// Un POST (ou un PATCH) sans clé d'idempotence n'est jamais rejoué : un seul essai
const methode = (options.method ?? 'GET').toUpperCase();
const rejouable = METHODES_IDEMPOTENTES.includes(methode) || new Headers(options.headers).has('Idempotency-Key');
if (!rejouable) essaisMax = 1;
for (let essai = 1; ; essai++) {
let reponse;
try {
reponse = await fetch(url, { ...options, signal: AbortSignal.timeout(5000) });
} catch (erreur) {
// Délai dépassé (TimeoutError) ou réseau coupé (TypeError) : passager lui aussi
if (essai === essaisMax) throw erreur;
await attendre(2 ** (essai - 1) * 1000 + Math.random() * 300);
continue;
}
if (!CODES_PASSAGERS.includes(reponse.status) || essai === essaisMax) return reponse;
// Retry-After en secondes si le serveur le donne, sinon 1 s, 2 s, 4 s… plus un peu de hasard
const retryAfter = Number(reponse.headers.get('Retry-After'));
const delai = retryAfter > 0 ? retryAfter * 1000 : 2 ** (essai - 1) * 1000 + Math.random() * 300;
await attendre(delai);
}
}
Testée contre un serveur qui répond 503, puis 429, puis 200, la fonction attend deux fois puis renvoie la réponse 200. Un POST sans en-tête Idempotency-Key ne fait qu'un essai : l'appelant reçoit le 503 et décide lui-même. Un serveur qui ne répond pas en cinq secondes déclenche une TimeoutError, traitée comme une erreur passagère ; au dernier essai, l'erreur remonte à l'appelant. Si Retry-After contient une date plutôt qu'un nombre de secondes, Number() donne NaN et la fonction se rabat sur l'attente exponentielle.
Ne jamais exposer un secret dans le client
Le code d'un front web se lit dans les outils de développement, et une application mobile se décompile en quelques minutes. Un secret placé dans le code d'un client (la clé d'API payante d'un service de cartographie, le secret OAuth d'une application) est donc un secret publié.
La règle : le client appelle ton serveur, et c'est ton serveur, qui seul détient la clé, qui appelle le service tiers. Ce schéma s'appelle un backend for frontend (BFF) ou, plus simplement, un proxy. Il permet aussi de contrôler qui appelle, de limiter le débit et de mettre les réponses en cache.
Front (navigateur) → API d'AppliFrais (détient la clé) → Service de géocodage
aucun secret contrôle le jeton de Lucas reçoit la clé d'API
Les seules valeurs acceptables dans un client sont publiques par nature : l'adresse de l'API, ou une clé expressément prévue pour être publique et restreinte à ton domaine par le fournisseur.
Exercices
Exercice 1 sur 5 Trouver les bogues Réussi À revoir
Ce code doit afficher les fiches clôturées d'octobre. Il contient quatre erreurs.
async function fichesCloturees() {
const reponse = fetch('https://api.gsb.example/v1/fiches?etat=CL&mois=' + mois);
const fiches = reponse.json();
fiches.donnees.forEach(f => afficher(f));
}
Voir la réponseMasquer la réponse de l'exercice 1
- Il manque
awaitdevantfetch:reponseest une promesse, pas une réponse. - Il manque
awaitdevantreponse.json(). - Le code ne teste pas
reponse.ok: un401ou un500serait traité comme une liste (etfiches.donneesseraitundefined, d'où uneTypeError). - L'en-tête
Authorizationest absent : l'API répondra401. (On peut aussi relever quemoisn'est ni défini ni encodé :URLSearchParamsserait plus sûr, et qu'aucun délai maximal n'est fixé.)
async function fichesCloturees(mois) {
const params = new URLSearchParams({ etat: 'CL', mois });
const reponse = await fetch(`https://api.gsb.example/v1/fiches?${params}`, {
headers: { Authorization: `Bearer ${lireJeton()}` },
signal: AbortSignal.timeout(5000),
});
if (!reponse.ok) throw new Error(`Erreur ${reponse.status}`);
const fiches = await reponse.json();
fiches.donnees.forEach((f) => afficher(f));
}
Tu avais trouvé ?
Exercice 2 sur 5 Réseau ou HTTP ? Réussi À revoir
Pour chaque situation, dis si fetch rejette sa promesse (le catch s'exécute) ou si elle se résout avec une réponse ; dans ce cas, donne reponse.ok et reponse.status.
- L'API répond
404. - Le Wi-Fi est coupé.
- L'API répond
201. - Le délai de
AbortSignal.timeout(5000)est dépassé. - L'API répond
503.
Voir la réponseMasquer la réponse de l'exercice 2
- Résolue :
okvautfalse,statusvaut404. - Rejetée (
TypeError) : il n'y a aucune réponse. - Résolue :
okvauttrue,statusvaut201. - Rejetée, avec une erreur dont le nom est
TimeoutError. - Résolue :
okvautfalse,statusvaut503.
Tu avais trouvé ?
Exercice 3 sur 5 Appeler une API en PHP Réussi À revoir
Écris le code PHP qui crée un frais hors forfait dans la fiche 12 avec la fonction appelerApi du chapitre (date 2026-10-14, libellé « Taxi gare », montant 18,40 €), puis affiche « Frais ajouté » pour un 201, la liste des erreurs de champs pour un 422, et « Fiche non modifiable » pour un 409.
Voir la réponseMasquer la réponse de l'exercice 3
<?php
$reponse = appelerApi('POST', 'https://api.gsb.example/v1/fiches/12/frais-hors-forfait', [
'date' => '2026-10-14',
'libelle' => 'Taxi gare',
'montant' => 18.40,
], $jeton);
switch ($reponse['statut']) {
case 201:
echo 'Frais ajouté';
break;
case 422:
foreach ($reponse['donnees']['erreurs'] ?? [] as $erreur) {
echo htmlspecialchars($erreur['champ'] . ' : ' . $erreur['message']), '<br>';
}
break;
case 409:
echo 'Fiche non modifiable';
break;
default:
echo 'Erreur ' . (int) $reponse['statut'];
}
La fonction lève une exception en cas d'échec réseau : dans une vraie page, l'appel serait entouré d'un try … catch (RuntimeException $e). Le montant 18.40 est envoyé en JSON comme 18.4, ce qui est la même valeur.
Tu avais trouvé ?
Exercice 4 sur 5 Combien de temps attendre ? Réussi À revoir
La fonction fetchAvecReessais du chapitre reçoit successivement trois réponses 503 sans en-tête Retry-After, puis une réponse 200. On ignore la part de hasard.
- Combien de temps attend-elle avant chaque nouvel essai ?
- Quel est le temps total d'attente ?
- Que se passe-t-il si le serveur répond
503aux quatre essais ? - Pourquoi ajoute-t-on une part de hasard ?
Voir la réponseMasquer la réponse de l'exercice 4
- Après l'essai 1 : 2⁰ × 1 000 = 1 000 ms ; après l'essai 2 : 2¹ × 1 000 = 2 000 ms ; après l'essai 3 : 2² × 1 000 = 4 000 ms. L'essai 4 reçoit
200. - 1 + 2 + 4 = 7 secondes (plus jusqu'à 3 × 300 ms de hasard).
- Au quatrième essai,
essai === essaisMax: la fonction renvoie la réponse503sans attendre davantage, et l'appelant doit afficher une erreur. - Si un incident coupe l'API pour mille clients en même temps, sans hasard ils reviendraient tous à la même milliseconde, et cette vague pourrait faire retomber le serveur. Le hasard étale les nouveaux essais.
Tu avais trouvé ?
Exercice 5 sur 5 Où placer la clé ? Réussi À revoir
Le front React d'AppliFrais doit afficher, pour chaque frais kilométrique, la distance calculée par un service de cartographie payant, qui exige une clé d'API secrète. Un développeur propose de la placer dans une variable d'environnement du front (VITE_CLE_CARTO). Que réponds-tu ?
Voir la réponseMasquer la réponse de l'exercice 5
Une variable d'environnement d'un front est injectée dans le code JavaScript au moment de la construction : n'importe qui la lit dans les outils de développement. La clé serait volée et utilisée aux frais du laboratoire. Il faut ajouter à l'API d'AppliFrais une route, par exemple GET /v1/distances?depart=…&arrivee=…, qui vérifie le jeton du visiteur, applique une limite de débit, puis appelle le service de cartographie depuis le serveur avec la clé stockée dans sa configuration. Le front n'appelle que l'API d'AppliFrais.
Tu avais trouvé ?