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!

Laisser un commentaire