Introduction : pourquoi un client HTTP correct est la base d'une application web moderne
Toute application web moderne interagit avec le serveur via HTTP. Qu'il s'agisse de charger une liste de produits, d'envoyer un formulaire d'inscription ou de récupérer des données pour un tableau de bord, tout se résume à l'envoi de requêtes HTTP. Avec TypeScript, grâce à son puissant système de types, on peut construire non seulement un client HTTP fonctionnel, mais véritablement fiable et prévisible.
Dans cet article, nous allons voir comment créer un client HTTP en TypeScript qui gérera les erreurs, typera correctement les réponses, gérera les en-têtes et supportera les middleware. Vous découvrirez pourquoi utiliser du fetch brut sans wrapper est une mauvaise pratique, et comment le transformer en un outil puissant.
Les bases : du fetch brut au client typé
Commençons par un exemple de base. Le fetch natif en JavaScript est un bon point de départ, mais il ne fournit pas un typage pratique ni une gestion des erreurs. Avec TypeScript, nous pouvons l'envelopper dans notre propre fonction.
Un wrapper simple pour fetch
Créons une classe de base HttpClient qui acceptera une URL de base et des paramètres.
interface RequestOptions { headers?: Record<string, string>; params?: Record<string, string>;}
class HttpClient { private baseUrl: string; private defaultHeaders: Record<string, string>;
constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) { this.baseUrl = baseUrl; this.defaultHeaders = defaultHeaders; }
private buildUrl(path: string, params?: Record<string, string>): string { const url = new URL(`${this.baseUrl}${path}`); if (params) { Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value)); } return url.toString(); }
async get<T>(path: string, options?: RequestOptions): Promise<T> { const url = this.buildUrl(path, options?.params); const response = await fetch(url, { method: 'GET', headers: { ...this.defaultHeaders, ...options?.headers }, });
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
return response.json() as Promise<T>; }}Maintenant, l'appel client.get<User>('/users/1') retournera un objet User typé. C'est déjà bien mieux que de travailler avec any.
Gestion avancée des erreurs et typage des réponses
Lancer simplement une erreur n'est pas la meilleure approche. Nous devons distinguer les erreurs réseau, les erreurs serveur (4xx, 5xx) et les erreurs d'analyse JSON. Créons nos propres classes d'erreur.
Création des classes d'erreur
class HttpError extends Error { status: number; body: any;
constructor(message: string, status: number, body?: any) { super(message); this.name = 'HttpError'; this.status = status; this.body = body; }}
class NetworkError extends Error { constructor(message: string) { super(message); this.name = 'NetworkError'; }}
class ParseError extends Error { constructor(message: string) { super(message); this.name = 'ParseError'; }}Gestion améliorée dans la méthode get
async get<T>(path: string, options?: RequestOptions): Promise<T> { try { const url = this.buildUrl(path, options?.params); const response = await fetch(url, { method: 'GET', headers: { ...this.defaultHeaders, ...options?.headers }, });
let body: any; try { body = await response.json(); } catch { throw new ParseError('Failed to parse response as JSON'); }
if (!response.ok) { throw new HttpError( `Request failed with status ${response.status}`, response.status, body ); }
return body as T; } catch (er