Client HTTP en TypeScript : faire des requêtes correctement de A à Z
\n\nLe développement web moderne est impensable sans interaction avec des serveurs distants. Que ce soit pour récupérer des données d'une API REST, envoyer des formulaires ou télécharger des fichiers — les requêtes HTTP sont le fondement de l'architecture client-serveur. TypeScript, avec son typage strict et ses outils puissants, permet de rendre le travail avec les requêtes HTTP non seulement sûr, mais aussi facile à maintenir. Dans cet article, nous allons voir comment créer un client HTTP bien conçu en TypeScript, éviter les erreurs courantes et écrire un code qui fonctionne de manière prévisible.
\n\n1. Les bases : fetch API et son typage
\n\nIntégré au navigateur, fetch est un outil moderne et flexible pour effectuer des requêtes HTTP. Cependant, son utilisation en TypeScript nécessite un typage correct de la réponse. Par défaut, fetch retourne Promise<Response>, et nous devons spécifier explicitement les données que nous attendons.
interface User {\n id: number;\n name: string;\n email: string;\n}\n\nasync function getUser(id: number): Promise<User> {\n const response = await fetch(`https://api.example.com/users/${id}`);\n \n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n \n const data: User = await response.json();\n return data;\n}\n\n\nNotez la vérification de response.ok. C'est crucial : même si le serveur renvoie une erreur (404, 500, etc.), fetch ne lèvera pas d'exception automatiquement. Nous devons gérer le statut manuellement.
Pour les requêtes POST avec un corps, il est nécessaire de spécifier l'en-tête Content-Type :
async function createUser(userData: Omit<User, 'id'>): Promise<User> {\n const response = await fetch('https://api.example.com/users', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(userData),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create user: ${response.statusText}`);\n }\n\n return response.json();\n}\n\n\n2. Création d'un client HTTP universel
\n\nAu lieu d'écrire fetch à chaque fois depuis zéro, il est préférable de créer une enveloppe — un client HTTP universel. Cela permet de gérer de manière centralisée l'URL de base, les en-têtes et la gestion des erreurs. Voici un exemple basé sur les classes :
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n\ninterface RequestConfig<TBody = unknown> {\n method: HttpMethod;\n url: string;\n body?: TBody;\n headers?: Record<string, string>;\n params?: Record<string, string>;\n}\n\nclass HttpClient {\n private baseUrl: string;\n private defaultHeaders: Record<string, string>;\n\n constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {\n this.baseUrl = baseUrl;\n this.defaultHeaders = {\n 'Content-Type': 'application/json',\n ...defaultHeaders,\n };\n }\n\n private async request<TResponse, TBody = unknown>(\n config: RequestConfig<TBody>\n ): Promise<TResponse> {\n const url = new URL(`${this.baseUrl}${config.url}`);\n \n if (config.params) {\n Object.entries(config.params).forEach(([key, value]) => {\n url.searchParams.append(key, value);\n });\n }\n\n const response = await fetch(url.toString(), {\n method: config.method,\n headers: { ...this.defaultHeaders, ...config.headers },\n body: config.body ? JSON.stringify(config.body) : undefined,\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(`Request failed: ${response.status} ${response.statusText}. Body: ${errorBody}`);\n }\n\n // Pour DELETE ou d'autres méthodes qui peuvent ne pas retourner de corps\n if (response.status === 204) {\n return undefined as TResponse;\n }\n\n return response.jso