Développement Web

Guide pratique : Les requêtes AJAX avec JavaScript

13 min de lecture

Prérequis

  • Niveau : bases JS, notion d’asynchrone (Promesses, async/await).
  • Outils : VS Code, navigateur (DevTools onglet Network).
  • Temps estimé : 1 h.

Pourquoi AJAX en 2026 (informations vérifiées en avril 2026, susceptibles d’évoluer) ?

AJAX permet de communiquer avec un serveur sans recharger la page : c’est la base de toute application moderne (recherche live, soumission de formulaire, infinite scroll, dashboards en temps réel). En 2026, on n’utilise plus XMLHttpRequest : fetch() est l’API standard et bien plus lisible.

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 :

  1. fetch() envoie une requête GET à l’URL et retourne une Promesse
  2. response.ok vérifie que le statut HTTP est entre 200 et 299
  3. response.json() parse le corps de la réponse en objet JavaScript
  4. .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éfaut
  • Content-Type: application/json — dit au serveur que le body est du JSON
  • JSON.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.

Erreurs fréquentes

fetch qui ne throw pas sur 404/500

Cause : seules les erreurs réseau déclenchent catch. Un statut 4xx/5xx est considéré « OK ».
Solution : testez response.ok et throw manuellement.

CORS error en console

Cause : le serveur n’autorise pas votre origine.
Solution : côté serveur, configurez Access-Control-Allow-Origin. En dev, utilisez un proxy (ex : Vite proxy).

Headers Content-Type sur FormData

Cause : on force Content-Type: multipart/form-data manuellement.
Solution : ne le mettez PAS — le navigateur ajoute le bon boundary automatiquement.

Pas de timeout, requête qui pend

Cause : par défaut, fetch n’a aucun timeout.
Solution : utilisez signal: AbortSignal.timeout(10000) (Chrome 103+, FF 100+, Safari 16+) ou un AbortController classique.

Exercice pratique

  1. Créez une page qui récupère et affiche une liste de 10 utilisateurs depuis https://jsonplaceholder.typicode.com/users
  2. Ajoutez un formulaire pour créer un nouveau post (POST vers /posts)
  3. Affichez un spinner de chargement pendant la requête
  4. Gérez les erreurs avec un message visible à l’utilisateur
  5. Bonus : ajoutez une recherche en temps réel avec debounce

Pour approfondir

Étape 1 : comprendre AJAX en 2026 — fetch a remplacé XMLHttpRequest

AJAX (Asynchronous JavaScript And XML) désigne historiquement la technique permettant à une page web de dialoguer avec un serveur sans recharger l’ensemble. En 2026, plus personne n’utilise XML : on échange du JSON. Et plus personne (ou presque) n’utilise XMLHttpRequest : l’API fetch native du navigateur l’a remplacée pour 95 % des cas. Tous les navigateurs modernes la supportent depuis 2017.

Pour un développeur à Dakar, Abidjan ou Cotonou qui maintient encore du code legacy, savoir convertir XHR en fetch est une compétence rentable : moins de lignes, gestion des promesses native, meilleure intégration avec async/await. Ce tutoriel construit pas-à-pas une mini app d’annuaire avec filtres et soumission de formulaire, en pur fetch sans framework.

Pré-requis : un éditeur (VS Code), Node.js 22 LTS pour servir un mini backend en JSON, et un navigateur récent. Si vous n’avez pas Node, un simple python3 -m http.server sur le dossier suffit pour servir le HTML statique.

Étape 2 : préparer le mini backend JSON

Pour tester nos appels AJAX, montons un backend en 30 lignes avec Express 5. Il expose deux routes : GET /api/contacts (liste) et POST /api/contacts (création).

// server.js
import express from "express";
const app = express();
app.use(express.json());
app.use(express.static("public"));

let contacts = [
  { id: 1, nom: "Diop", ville: "Dakar" },
  { id: 2, nom: "Kouassi", ville: "Abidjan" }
];

app.get("/api/contacts", (req, res) => {
  const q = (req.query.q || "").toLowerCase();
  res.json(contacts.filter(c =>
    c.nom.toLowerCase().includes(q) || c.ville.toLowerCase().includes(q)
  ));
});

app.post("/api/contacts", (req, res) => {
  const c = { id: Date.now(), ...req.body };
  contacts.push(c);
  res.status(201).json(c);
});

app.listen(3000, () => console.log("API sur http://localhost:3000"));

Démarrez : node server.js. Sortie de référence : « API sur http://localhost:3000 ». Testez avec curl http://localhost:3000/api/contacts, vous devez voir le tableau JSON. Si Node refuse l’import ESM, ajoutez "type":"module" dans package.json.

Étape 3 : premier GET avec fetch et async/await

Créez public/index.html avec une liste vide et un bouton « Charger ». Notre premier fetch ramène la liste complète et l’affiche.

<!-- public/index.html -->
<!doctype html>
<meta charset="utf-8">
<title>Annuaire AJAX</title>
<button id="charger">Charger les contacts</button>
<ul id="liste"></ul>
<script type="module" src="app.js"></script>
// public/app.js
const liste = document.getElementById("liste");

async function charger() {
  try {
    const reponse = await fetch("/api/contacts");
    if (!reponse.ok) throw new Error("HTTP " + reponse.status);
    const contacts = await reponse.json();
    liste.innerHTML = contacts
      .map(c => `<li>${c.nom} — ${c.ville}</li>`)
      .join("");
  } catch (e) {
    liste.innerHTML = `<li>Erreur : ${e.message}</li>`;
  }
}

document.getElementById("charger").addEventListener("click", charger);

Sortie de référence : au clic, la liste se remplit avec « Diop — Dakar » et « Kouassi — Abidjan ». Si vous voyez l’erreur HTTP, vérifiez que le backend tourne et que vous accédez bien à http://localhost:3000 (pas un fichier file://).

Étape 4 : ajouter des paramètres de requête (search)

Ajoutons un champ de recherche qui filtre côté serveur. On utilise URLSearchParams pour construire l’URL proprement et éviter les bugs d’encodage.

// Ajout dans index.html
<input id="recherche" placeholder="Filtrer par nom ou ville">

// Ajout dans app.js
const recherche = document.getElementById("recherche");

async function chargerFiltre(q) {
  const params = new URLSearchParams({ q: q || "" });
  const reponse = await fetch(`/api/contacts?${params}`);
  const contacts = await reponse.json();
  liste.innerHTML = contacts
    .map(c => `<li>${c.nom} — ${c.ville}</li>`)
    .join("");
}

let timer;
recherche.addEventListener("input", e => {
  clearTimeout(timer);
  timer = setTimeout(() => chargerFiltre(e.target.value), 300);
});

Ce que vous devez voir : tapez « dak » et la liste se restreint à Diop. Le setTimeout de 300 ms (debounce) évite d’envoyer une requête à chaque touche frappée. Sans debounce, taper « Dakar » envoie 5 requêtes au lieu d’une seule, ce qui surcharge inutilement le réseau (sensible en 4G mobile).

Étape 5 : envoyer un POST avec un payload JSON

Ajoutons un formulaire pour créer un contact. Le piège classique : oublier le header Content-Type ou ne pas sérialiser le corps en JSON.

<form id="formulaire">
  <input name="nom" required placeholder="Nom">
  <input name="ville" required placeholder="Ville">
  <button type="submit">Créer</button>
</form>
document.getElementById("formulaire").addEventListener("submit", async e => {
  e.preventDefault();
  const data = Object.fromEntries(new FormData(e.target));
  const reponse = await fetch("/api/contacts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data)
  });
  if (!reponse.ok) {
    alert("Erreur " + reponse.status);
    return;
  }
  e.target.reset();
  chargerFiltre(recherche.value);
});

Résultat attendu : remplissez nom= »Sow », ville= »Saint-Louis », validez. La liste se rafraîchit avec la nouvelle entrée. Si le backend renvoie 415 Unsupported Media Type, c’est que le header Content-Type est manquant ou mal écrit.

Étape 6 : gérer les erreurs réseau et HTTP proprement

Une faute fréquente : croire que fetch rejette la promesse en cas d’erreur HTTP. C’est faux. Fetch ne rejette QUE pour des erreurs réseau (DNS, offline, CORS bloqué). Pour 404 ou 500, vous devez vérifier response.ok manuellement.

async function fetchSafe(url, options) {
  let reponse;
  try {
    reponse = await fetch(url, options);
  } catch (e) {
    throw new Error("Reseau indisponible (verifier connexion)");
  }
  if (!reponse.ok) {
    const corps = await reponse.text();
    throw new Error(`HTTP ${reponse.status} : ${corps.slice(0, 200)}`);
  }
  return reponse.json();
}

Ce que vous devez voir : utilisez await fetchSafe("/api/contacts") partout. Vous obtenez un message clair en cas d’offline (fréquent sur les connexions 3G en zone rurale ouest-africaine) et un message HTTP exploitable en cas d’erreur serveur.

Étape 7 : annuler une requête avec AbortController

Si l’utilisateur tape vite ou change de page, vous voulez annuler la requête en cours pour économiser bande passante et batterie. AbortController est l’outil natif depuis 2017.

let controller;
async function chercher(q) {
  if (controller) controller.abort();
  controller = new AbortController();
  try {
    const reponse = await fetch(`/api/contacts?q=${encodeURIComponent(q)}`, {
      signal: controller.signal
    });
    const data = await reponse.json();
    afficher(data);
  } catch (e) {
    if (e.name === "AbortError") return; // requête remplacée
    console.error(e);
  }
}

Ce que vous devez voir : tapez très vite « dakar », seule la dernière requête arrive jusqu’au DOM. Les requêtes intermédiaires sont coupées proprement, l’onglet Network du devtools le montre en gris (canceled).

Étape 8 : uploader un fichier avec FormData

Pour envoyer un fichier (avatar, photo de profil), utilisez FormData. Surtout, NE METTEZ PAS le header Content-Type vous-même : le navigateur doit le générer pour inclure le boundary multipart.

<input type="file" id="avatar" accept="image/*">
document.getElementById("avatar").addEventListener("change", async e => {
  const file = e.target.files[0];
  if (!file) return;
  if (file.size > 2 * 1024 * 1024) {
    alert("Fichier > 2 Mo, refuse pour economiser data");
    return;
  }
  const data = new FormData();
  data.append("avatar", file);
  const r = await fetch("/api/upload", { method: "POST", body: data });
  if (r.ok) console.log("Upload OK");
});

Vous devriez obtenir : sélectionnez une image < 2 Mo, vérifiez dans Network que la requête part en multipart/form-data. La limite à 2 Mo est volontaire : sur connexion mobile à 1 Mbit/s ascendant (cas fréquent au Sénégal hors fibre), 2 Mo prennent déjà 16 secondes à uploader.

Étape 9 : maîtriser CORS quand le backend est sur un autre domaine

Si votre frontend est servi depuis app.votre-domaine.io et votre API depuis api.votre-domaine.io, le navigateur applique CORS. Sans header Access-Control-Allow-Origin côté serveur, le fetch échoue avec un message clair en console.

// Côté backend Express
import cors from "cors";
app.use(cors({
  origin: ["https://app.votre-domaine.io"],
  credentials: true
}));

Pour envoyer un cookie de session avec votre fetch, ajoutez credentials: "include" côté client. Sans cela, même si le cookie existe, fetch ne l’enverra pas vers un autre domaine. C’est la cause numéro 1 des « j’ai bien le cookie mais je suis quand même 401 ».

Étape 10 : tester, mesurer et optimiser pour réseau lent

En Afrique de l’Ouest, beaucoup d’utilisateurs sont sur 3G ou Wi-Fi lent en agence. Testez votre app dans ces conditions avant de la livrer.

# Devtools Chrome → onglet Network → throttling Slow 3G
# Rechargez la page, ouvrez votre app, mesurez :
# - temps de chargement initial
# - taille des réponses JSON
# - nombre de requêtes par interaction

Trois optimisations qui paient toujours : 1) gzip ou brotli côté serveur (Express via compression), 2) cache HTTP avec Cache-Control: max-age=3600 sur les listes peu volatiles, 3) pagination côté serveur (jamais retourner 10 000 lignes en JSON, paginer par 50).

Pour creuser ce sujet sur le développement web côté client, consultez notre tutoriel JavaScript moderne ES2025 et le guide API REST Express pas-à-pas. Vous serez équipé pour bâtir des applications réactives qui tiennent la charge sur réseaux contraints.

Partager