Skip to main content

Code that compiles on the first try! CodeWithMpia wishes you very happy holidays.✨

Aller au contenu principal

Maîtriser l'API Fetch : guide complet pour des requêtes HTTP modernes en JavaScript

Guide pratique et moderne pour maîtriser l’API Fetch en JavaScript : requêtes HTTP GET, POST, PUT et DELETE, gestion des erreurs, async/await, authentification, timeout, retry, cache, rate limiting et intégration avec des frameworks frontend.

M
Mpia
12/17/2025 33 min
105 vues
0 comme.
#JS#API#Fetch#HTTP
Maîtriser l'API Fetch : guide complet pour des requêtes HTTP modernes en JavaScript

Salut ! Aujourd'hui, on va plonger dans l'une des APIs les plus utilisées en développement web : Fetch. Si tu développes des applications web modernes, tu vas forcément avoir besoin de communiquer avec des APIs. Et Fetch est l'outil parfait pour ça.

Dans cet article, je vais te montrer comment passer de débutant à pro avec Fetch, en couvrant tous les cas d'usage : des requêtes simples aux patterns avancés que tu trouveras dans des applications production.

Pourquoi Fetch plutôt que XMLHttpRequest ?

Avant Fetch, on utilisait XMLHttpRequest. C'était... pas terrible. Regarde la différence :

L'ancienne façon (XMLHttpRequest)

// Beurk... 😬
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users');
xhr.onload = function() {
  if (xhr.status === 200) {
    const data = JSON.parse(xhr.responseText);
    console.log(data);
  }
};
xhr.onerror = function() {
  console.error('Erreur réseau');
};
xhr.send();

La façon moderne (Fetch)

// Beaucoup mieux ! 😎
fetch('https://api.example.com/users')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Erreur:', error));

Fetch est basé sur les Promises, ce qui le rend plus lisible et plus facile à utiliser avec async/await. En plus, il est natif dans tous les navigateurs modernes !

Les bases : requête GET simple

Commençons par le plus simple. Récupérer des données d'une API.

Version Promise

fetch('https://jsonplaceholder.typicode.com/posts')
  .then(response => {
    // Vérifier que la requête a réussi
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then(posts => {
    console.log('Articles récupérés:', posts);
    posts.forEach(post => {
      console.log(`${post.id}: ${post.title}`);
    });
  })
  .catch(error => {
    console.error('Erreur lors de la récupération:', error);
  });

Version async/await (plus propre)

async function recupererPosts() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const posts = await response.json();
    console.log('Articles récupérés:', posts);
    return posts;
  } catch (error) {
    console.error('Erreur lors de la récupération:', error);
    throw error;
  }
}

// Utilisation
recupererPosts();

Requêtes POST : envoyer des données

Pour créer des ressources ou envoyer des formulaires, on utilise POST.

Envoyer du JSON

async function creerPost(titre, contenu) {
  try {
    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
      })
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const nouveauPost = await response.json();
    console.log('Post créé:', nouveauPost);
    return nouveauPost;
  } catch (error) {
    console.error('Erreur lors de la création:', error);
    throw error;
  }
}

// Utilisation
creerPost('Mon titre', 'Mon super contenu');

Envoyer des données de formulaire

async function envoyerFormulaire(formData) {
  try {
    const response = await fetch('https://api.example.com/upload', {
      method: 'POST',
      body: formData // FormData est automatiquement détecté
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const resultat = await response.json();
    return resultat;
  } catch (error) {
    console.error('Erreur lors de l\'envoi:', error);
    throw error;
  }
}

// Utilisation avec un formulaire HTML
const form = document.querySelector('#mon-formulaire');
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const formData = new FormData(form);
  await envoyerFormulaire(formData);
});

Upload de fichiers

async function uploaderFichier(fichier) {
  const formData = new FormData();
  formData.append('file', fichier);
  formData.append('description', 'Mon fichier uploadé');

  try {
    const response = await fetch('https://api.example.com/upload', {
      method: 'POST',
      body: formData
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const resultat = await response.json();
    console.log('Fichier uploadé:', resultat);
    return resultat;
  } catch (error) {
    console.error('Erreur d\'upload:', error);
    throw error;
  }
}

// Utilisation avec un input file
const inputFile = document.querySelector('#file-input');
inputFile.addEventListener('change', async (e) => {
  const fichier = e.target.files[0];
  if (fichier) {
    await uploaderFichier(fichier);
  }
});

PUT et DELETE : mettre à jour et supprimer

Mettre à jour une ressource (PUT)

async function mettreAJourPost(id, donneesModifiees) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(donneesModifiees)
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const postMisAJour = await response.json();
    console.log('Post mis à jour:', postMisAJour);
    return postMisAJour;
  } catch (error) {
    console.error('Erreur lors de la mise à jour:', error);
    throw error;
  }
}

// Utilisation
mettreAJourPost(1, {
  title: 'Titre modifié',
  body: 'Contenu modifié'
});

Supprimer une ressource (DELETE)

async function supprimerPost(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
      method: 'DELETE'
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    console.log(`Post ${id} supprimé avec succès`);
    return true;
  } catch (error) {
    console.error('Erreur lors de la suppression:', error);
    throw error;
  }
}

// Utilisation
supprimerPost(1);

Gestion avancée des erreurs

La gestion d'erreurs avec Fetch peut être trompeuse. Fetch ne rejette la Promise que pour les erreurs réseau, pas pour les erreurs HTTP !

Fonction utilitaire robuste

async function fetchAvecGestionErreurs(url, options = {}) {
  try {
    const response = await fetch(url, options);

    // Fetch ne rejette pas automatiquement les erreurs HTTP
    if (!response.ok) {
      // Essayer de récupérer le message d'erreur de l'API
      let errorMessage = `HTTP error! status: ${response.status}`;

      try {
        const errorData = await response.json();
        errorMessage = errorData.message || errorMessage;
      } catch {
        // Si pas de JSON, utiliser le texte
        const errorText = await response.text();
        errorMessage = errorText || errorMessage;
      }

      throw new Error(errorMessage);
    }

    return response;
  } catch (error) {
    // Erreur réseau ou autre
    if (error.name === 'TypeError') {
      throw new Error('Erreur réseau : impossible de contacter le serveur');
    }
    throw error;
  }
}

// Utilisation
async function recupererUtilisateur(id) {
  try {
    const response = await fetchAvecGestionErreurs(
      `https://api.example.com/users/${id}`
    );
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Erreur:', error.message);
    // Afficher un message à l'utilisateur
    afficherNotification('Erreur lors de la récupération des données', 'error');
    throw error;
  }
}

Headers et authentification

Ajouter des headers personnalisés

async function fetchAvecAuth(url, data = null) {
  const options = {
    method: data ? 'POST' : 'GET',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${localStorage.getItem('token')}`,
      'X-Custom-Header': 'valeur-personnalisee'
    }
  };

  if (data) {
    options.body = JSON.stringify(data);
  }

  try {
    const response = await fetch(url, options);

    if (response.status === 401) {
      // Token expiré, rediriger vers login
      window.location.href = '/login';
      throw new Error('Session expirée');
    }

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Erreur:', error);
    throw error;
  }
}

Système d'authentification complet

class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.token = localStorage.getItem('token');
  }

  setToken(token) {
    this.token = token;
    localStorage.setItem('token', token);
  }

  clearToken() {
    this.token = null;
    localStorage.removeItem('token');
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;

    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      }
    };

    if (this.token) {
      config.headers['Authorization'] = `Bearer ${this.token}`;
    }

    try {
      const response = await fetch(url, config);

      if (response.status === 401) {
        this.clearToken();
        window.location.href = '/login';
        throw new Error('Non authentifié');
      }

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || 'Erreur serveur');
      }

      return await response.json();
    } catch (error) {
      console.error('Erreur API:', error);
      throw error;
    }
  }

  async get(endpoint) {
    return this.request(endpoint, { method: 'GET' });
  }

  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }

  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }

  async delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

// Utilisation
const api = new ApiClient('https://api.example.com');

// Login
const loginData = await api.post('/auth/login', {
  email: 'user@example.com',
  password: 'password'
});
api.setToken(loginData.token);

// Utiliser l'API
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Mpia' });
await api.put('/users/1', { name: 'Mpia M.' });
await api.delete('/users/1');

Timeout et AbortController

Par défaut, Fetch n'a pas de timeout. Voici comment en ajouter un :

Timeout simple

async function fetchAvecTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error('Requête annulée : timeout dépassé');
    }
    throw error;
  }
}

// Utilisation
try {
  const response = await fetchAvecTimeout('https://api.example.com/data', {}, 3000);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error('Erreur:', error.message);
}

Annulation manuelle

let currentController = null;

async function rechercherUtilisateurs(query) {
  // Annuler la requête précédente si elle existe
  if (currentController) {
    currentController.abort();
  }

  currentController = new AbortController();

  try {
    const response = await fetch(
      `https://api.example.com/users/search?q=${query}`,
      { signal: currentController.signal }
    );

    const resultats = await response.json();
    afficherResultats(resultats);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Recherche annulée');
    } else {
      console.error('Erreur de recherche:', error);
    }
  }
}

// Utilisation dans un champ de recherche
const champRecherche = document.querySelector('#recherche');
champRecherche.addEventListener('input', (e) => {
  rechercherUtilisateurs(e.target.value);
});

Requêtes parallèles et séquentielles

Requêtes en parallèle avec Promise.all

async function recupererToutesDonnees() {
  try {
    // Lancer toutes les requêtes en même temps
    const [users, posts, comments] = await Promise.all([
      fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json()),
      fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json()),
      fetch('https://jsonplaceholder.typicode.com/comments').then(r => r.json())
    ]);

    return { users, posts, comments };
  } catch (error) {
    console.error('Erreur lors du chargement:', error);
    throw error;
  }
}

// Utilisation
const data = await recupererToutesDonnees();
console.log('Toutes les données:', data);

Promise.allSettled pour gérer les échecs

async function recupererDonneesMultiples() {
  const urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments'
  ];

  const promises = urls.map(url => 
    fetch(url)
      .then(r => r.json())
      .catch(error => ({ error: error.message }))
  );

  const results = await Promise.allSettled(promises);

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Requête ${index} réussie:`, result.value);
    } else {
      console.error(`Requête ${index} échouée:`, result.reason);
    }
  });

  return results;
}

Requêtes séquentielles

async function traiterUtilisateursSequentiellement(userIds) {
  const resultats = [];

  for (const id of userIds) {
    try {
      const response = await fetch(`https://api.example.com/users/${id}`);
      const user = await response.json();
      resultats.push(user);

      // Attendre un peu entre chaque requête pour ne pas surcharger l'API
      await new Promise(resolve => setTimeout(resolve, 100));
    } catch (error) {
      console.error(`Erreur pour l'utilisateur ${id}:`, error);
    }
  }

  return resultats;
}

Retry et rate limiting

Système de retry automatique

async function fetchAvecRetry(url, options = {}, maxRetries = 3) {
  let lastError;

  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      return response;
    } catch (error) {
      lastError = error;
      console.log(`Tentative ${i + 1} échouée, retry...`);

      // Attendre avant de réessayer (backoff exponentiel)
      const delai = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delai));
    }
  }

  throw new Error(`Échec après ${maxRetries} tentatives: ${lastError.message}`);
}

// Utilisation
try {
  const response = await fetchAvecRetry('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error('Impossible de récupérer les données:', error);
}

Rate limiting simple

class RateLimiter {
  constructor(maxRequests, perMilliseconds) {
    this.maxRequests = maxRequests;
    this.perMilliseconds = perMilliseconds;
    this.requests = [];
  }

  async waitForSlot() {
    const now = Date.now();

    // Nettoyer les anciennes requêtes
    this.requests = this.requests.filter(
      time => now - time < this.perMilliseconds
    );

    if (this.requests.length >= this.maxRequests) {
      // Attendre que le slot le plus ancien expire
      const oldestRequest = this.requests[0];
      const waitTime = this.perMilliseconds - (now - oldestRequest);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      return this.waitForSlot();
    }

    this.requests.push(now);
  }

  async fetch(url, options) {
    await this.waitForSlot();
    return fetch(url, options);
  }
}

// Utilisation : max 5 requêtes par seconde
const limiter = new RateLimiter(5, 1000);

async function recupererDonneesAvecLimite(ids) {
  const promises = ids.map(async (id) => {
    const response = await limiter.fetch(`https://api.example.com/users/${id}`);
    return response.json();
  });

  return Promise.all(promises);
}

Cache et optimisation

Cache simple avec Map

class CachedFetch {
  constructor(cacheDuration = 5 * 60 * 1000) { // 5 minutes par défaut
    this.cache = new Map();
    this.cacheDuration = cacheDuration;
  }

  getCacheKey(url, options) {
    return `${url}-${JSON.stringify(options)}`;
  }

  async fetch(url, options = {}) {
    const key = this.getCacheKey(url, options);
    const cached = this.cache.get(key);

    if (cached && Date.now() - cached.timestamp < this.cacheDuration) {
      console.log('Données récupérées du cache');
      return cached.data;
    }

    console.log('Fetch depuis le serveur');
    const response = await fetch(url, options);
    const data = await response.json();

    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });

    return data;
  }

  clearCache() {
    this.cache.clear();
  }

  invalidateCache(url) {
    for (const key of this.cache.keys()) {
      if (key.startsWith(url)) {
        this.cache.delete(key);
      }
    }
  }
}

// Utilisation
const cachedFetch = new CachedFetch();

// Premier appel : fetch depuis le serveur
const users1 = await cachedFetch.fetch('https://api.example.com/users');

// Deuxième appel dans les 5 minutes : depuis le cache
const users2 = await cachedFetch.fetch('https://api.example.com/users');

// Invalider le cache pour cette URL
cachedFetch.invalidateCache('https://api.example.com/users');

Intercepteurs (comme Axios)

class FetchInterceptor {
  constructor() {
    this.requestInterceptors = [];
    this.responseInterceptors = [];
  }

  addRequestInterceptor(interceptor) {
    this.requestInterceptors.push(interceptor);
  }

  addResponseInterceptor(interceptor) {
    this.responseInterceptors.push(interceptor);
  }

  async fetch(url, options = {}) {
    // Appliquer les intercepteurs de requête
    let modifiedOptions = { ...options };
    for (const interceptor of this.requestInterceptors) {
      const result = await interceptor(url, modifiedOptions);
      if (result) {
        modifiedOptions = result;
      }
    }

    // Faire la requête
    let response = await fetch(url, modifiedOptions);

    // Appliquer les intercepteurs de réponse
    for (const interceptor of this.responseInterceptors) {
      response = await interceptor(response);
    }

    return response;
  }
}

// Utilisation
const fetchClient = new FetchInterceptor();

// Ajouter un token à toutes les requêtes
fetchClient.addRequestInterceptor((url, options) => {
  const token = localStorage.getItem('token');
  if (token) {
    options.headers = {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    };
  }
  console.log(`Requête vers ${url}`);
  return options;
});

// Logger toutes les réponses
fetchClient.addResponseInterceptor(async (response) => {
  console.log(`Réponse reçue: ${response.status}`);
  return response;
});

// Utiliser le client
const response = await fetchClient.fetch('https://api.example.com/users');
const data = await response.json();

Intégration avec un framework frontend

Avec Vue 3 (Composition API)

// composables/useFetch.js
import { ref, computed } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(false);

  const execute = async () => {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e.message;
    } finally {
      loading.value = false;
    }
  };

  return {
    data,
    error,
    loading,
    execute
  };
}

// Utilisation dans un composant
import { useFetch } from '@/composables/useFetch';

export default {
  setup() {
    const { data: users, error, loading, execute } = useFetch(
      'https://jsonplaceholder.typicode.com/users'
    );

    // Charger au montage
    execute();

    return { users, error, loading };
  }
};

Avec Svelte

// stores/api.js
import { writable } from 'svelte/store';

export function createFetchStore(url) {
  const { subscribe, set, update } = writable({
    data: null,
    loading: false,
    error: null
  });

  const load = async () => {
    update(state => ({ ...state, loading: true }));

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      set({ data, loading: false, error: null });
    } catch (error) {
      set({ data: null, loading: false, error: error.message });
    }
  };

  return {
    subscribe,
    load
  };
}

// Utilisation dans un composant Svelte
import { createFetchStore } from './stores/api';

const users = createFetchStore('https://jsonplaceholder.typicode.com/users');
users.load();

Bonnes pratiques

Voici mes recommandations pour utiliser Fetch comme un pro :

1. Toujours gérer les erreurs

// ❌ Mauvais
const data = await fetch(url).then(r => r.json());

// ✅ Bon
try {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP error! ${response.status}`);
  const data = await response.json();
} catch (error) {
  console.error('Erreur:', error);
  // Gérer l'erreur proprement
}

2. Centraliser la configuration

// config/api.js
export const API_BASE_URL = 'https://api.example.com';
export const API_TIMEOUT = 10000;

export const defaultHeaders = {
  'Content-Type': 'application/json',
};

export async function apiFetch(endpoint, options = {}) {
  const url = `${API_BASE_URL}${endpoint}`;
  const config = {
    ...options,
    headers: {
      ...defaultHeaders,
      ...options.headers
    }
  };

  const response = await fetch(url, config);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
}

3. Typage avec TypeScript

// types/api.ts
interface User {
  id: number;
  name: string;
  email: string;
}

interface ApiResponse<T> {
  data: T;
  message: string;
}

async function getUser(id: number): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  const result: ApiResponse<User> = await response.json();
  return result.data;
}

4. Variables d'environnement

// .env
VITE_API_URL=https://api.example.com
VITE_API_KEY=your-api-key

// api.js
const API_URL = import.meta.env.VITE_API_URL;
const API_KEY = import.meta.env.VITE_API_KEY;

async function secureFetch(endpoint) {
  return fetch(`${API_URL}${endpoint}`, {
    headers: {
      'X-API-Key': API_KEY
    }
  });
}

Conclusion

Fetch est un outil puissant et flexible pour gérer tes requêtes HTTP. En combinant les techniques qu'on a vues, tu peux créer un système de communication API robuste et maintenable.

Récapitulatif des points clés :

  • Utilise async/await pour un code plus lisible

  • Gère toujours les erreurs proprement (Fetch ne rejette pas automatiquement les erreurs HTTP)

  • Implémente un système de retry pour les requêtes importantes

  • Utilise AbortController pour les timeouts et annulations

  • Cache les données quand c'est pertinent

  • Crée une couche d'abstraction pour centraliser ta configuration

Avec ces techniques, tu es maintenant équipé pour gérer n'importe quelle API dans tes projets ! 🚀


Pour aller plus loin :

Tu as des questions ou des patterns Fetch que tu utilises ? Partage-les dans les commentaires!

Commentaires (0)

Laisser un commentaire

Aucun commentaire pour le moment. Soyez le premier à commenter !