Einleitung: Warum ein korrekter HTTP-Client die Grundlage moderner Webanwendungen ist
Jede moderne Webanwendung interagiert über HTTP mit dem Server. Ob das Laden einer Produktliste, das Absenden eines Registrierungsformulars oder das Abrufen von Daten für ein Dashboard – all das läuft auf das Senden von HTTP-Anfragen hinaus. Mit TypeScript und seinem leistungsstarken Typsystem kann man nicht nur einen funktionierenden, sondern einen wirklich zuverlässigen und vorhersagbaren HTTP-Client erstellen.
In diesem Artikel werden wir durchgehen, wie man einen HTTP-Client in TypeScript erstellt, der Fehler behandelt, Antworten korrekt typisiert, Header verwaltet und Middleware unterstützt. Sie werden erfahren, warum die Verwendung von rohem fetch ohne Wrapper eine schlechte Praxis ist und wie man es in ein leistungsstarkes Werkzeug verwandelt.
Grundlagen: Vom rohen fetch zum typisierten Client
Beginnen wir mit einem einfachen Beispiel. Der eingebaute fetch in JavaScript ist ein guter Ausgangspunkt, aber er bietet keine bequeme Typisierung und Fehlerbehandlung. In TypeScript können wir ihn in eine eigene Funktion einwickeln.
Einfacher Wrapper für fetch
Erstellen wir eine Basisklasse HttpClient, die eine Basis-URL und Einstellungen akzeptiert.
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>; }}Jetzt gibt der Aufruf client.get<User>('/users/1') ein typisiertes Objekt User zurück. Das ist bereits viel besser als die Arbeit mit any.
Fortgeschrittene Fehlerbehandlung und Typisierung von Antworten
Einfaches Auslösen eines Fehlers ist nicht der beste Ansatz. Wir müssen zwischen Netzwerkfehlern, Serverfehlern (4xx, 5xx) und Fehlern beim Parsen von JSON unterscheiden. Erstellen wir eigene Fehlerklassen.
Erstellung von Fehlerklassen
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'; }}Verbesserte Behandlung in der get-Methode
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