AJAX en 2026 : fetch() a remplacé XMLHttpRequest
AJAX (Asynchronous JavaScript and XML) permet à votre page web de communiquer avec un serveur sans rechargement. Aujourd’hui, on n’utilise plus l’ancien XMLHttpRequest — l’API fetch() est plus simple, plus lisible et supportée par tous les navigateurs modernes. Ce guide vous montre comment l’utiliser concrètement avec des exemples que vous pouvez tester immédiatement.
Votre première requête GET
// Récupérer des données depuis une API
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => {
if (!response.ok) {
throw new Error('Erreur HTTP : ' + response.status);
}
return response.json();
})
.then(data => {
console.log('Titre :', data.title);
console.log('Contenu :', data.body);
})
.catch(error => {
console.error('Erreur :', error.message);
});
Ce qui se passe :
fetch()envoie une requête GET à l’URL et retourne une Promesseresponse.okvérifie que le statut HTTP est entre 200 et 299response.json()parse le corps de la réponse en objet JavaScript.catch()attrape les erreurs réseau (pas les erreurs 404/500 !)
La même requête avec async/await
async function recupererArticle(id) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/' + id);
if (!response.ok) {
throw new Error('Article non trouvé (HTTP ' + response.status + ')');
}
const article = await response.json();
return article;
} catch (error) {
console.error('Erreur :', error.message);
return null;
}
}
// Utilisation
const article = await recupererArticle(1);
console.log(article.title);
async/await rend le code asynchrone aussi lisible que du code synchrone. Le try/catch gère les erreurs de manière claire.
Envoyer des données avec POST
async function creerArticle(titre, contenu) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: titre,
body: contenu,
userId: 1
})
});
const nouvelArticle = await response.json();
console.log('Article créé avec ID :', nouvelArticle.id);
return nouvelArticle;
}
creerArticle('Mon titre', 'Mon contenu ici');
Points clés du POST :
method: 'POST'— sans ça, fetch envoie un GET par défautContent-Type: application/json— dit au serveur que le body est du JSONJSON.stringify()— convertit l’objet JavaScript en chaîne JSON
PUT, PATCH et DELETE
// PUT : remplacer entièrement une ressource
await fetch('/api/articles/42', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Nouveau titre', body: 'Nouveau contenu' })
});
// PATCH : modifier partiellement
await fetch('/api/articles/42', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Titre corrigé' })
});
// DELETE : supprimer
await fetch('/api/articles/42', { method: 'DELETE' });
Envoyer un formulaire avec FormData
const form = document.getElementById('monFormulaire');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
// Pour uploader un fichier
const response = await fetch('/api/upload', {
method: 'POST',
body: formData // PAS de Content-Type ici — le navigateur le définit automatiquement
});
const result = await response.json();
console.log('Fichier uploadé :', result.url);
});
Important : quand vous envoyez un FormData, ne définissez PAS le header Content-Type. Le navigateur le fait automatiquement avec le bon boundary pour le multipart/form-data.
Gestion avancée des erreurs
async function fetchAvecGestion(url, options = {}) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(10000) // Timeout de 10 secondes
});
if (response.status === 401) {
// Rediriger vers la page de connexion
window.location.href = '/login';
return;
}
if (response.status === 404) {
console.warn('Ressource non trouvée :', url);
return null;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Erreur serveur ' + response.status);
}
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('La requête a pris trop de temps');
} else if (error.name === 'TypeError') {
console.error('Problème réseau — vérifiez votre connexion');
} else {
console.error('Erreur :', error.message);
}
return null;
}
}
Exemple complet : recherche en temps réel
const searchInput = document.getElementById('search');
const resultsList = document.getElementById('results');
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const query = e.target.value.trim();
if (query.length < 2) {
resultsList.innerHTML = '';
return;
}
const data = await fetchAvecGestion(
'/api/search?q=' + encodeURIComponent(query)
);
if (data) {
resultsList.innerHTML = data.results
.map(r => '<li>' + r.title + '</li>')
.join('');
}
}, 300); // Attendre 300ms après la dernière frappe
});
Le debounce évite d’envoyer une requête à chaque caractère tapé. On attend que l’utilisateur arrête de taper pendant 300ms avant d’envoyer la requête. Essayez sans debounce, puis avec, pour voir la différence dans l’onglet Network de DevTools.
Exercice pratique
- Créez une page qui récupère et affiche une liste de 10 utilisateurs depuis
https://jsonplaceholder.typicode.com/users - Ajoutez un formulaire pour créer un nouveau post (POST vers /posts)
- Affichez un spinner de chargement pendant la requête
- Gérez les erreurs avec un message visible à l’utilisateur
- Bonus : ajoutez une recherche en temps réel avec debounce