HTTP-Client in TypeScript: Anfragen richtig stellen von A bis Z
\n\nModerne Webentwicklung ist ohne Interaktion mit entfernten Servern undenkbar. Ob das Abrufen von Daten von einer REST-API, das Senden von Formularen oder das Hochladen von Dateien – HTTP-Anfragen sind die Grundlage der Client-Server-Architektur. TypeScript ermöglicht mit seiner strengen Typisierung und leistungsstarken Werkzeugen, die Arbeit mit HTTP-Anfragen nicht nur sicher, sondern auch wartungsfreundlich zu gestalten. In diesem Artikel werden wir uns ansehen, wie man einen durchdachten HTTP-Client in TypeScript erstellt, typische Fehler vermeidet und Code schreibt, der vorhersagbar funktioniert.
\n\n1. Grundlagen: fetch API und ihre Typisierung
\n\nDer in den Browser integrierte fetch ist ein modernes und flexibles Werkzeug zur Durchführung von HTTP-Anfragen. Seine Verwendung in TypeScript erfordert jedoch eine korrekte Typisierung der Antwort. Standardmäßig gibt fetch ein Promise<Response> zurück, und wir müssen explizit angeben, welche Daten wir erwarten.
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\nBeachten Sie die Überprüfung von response.ok. Dies ist entscheidend: Selbst wenn der Server einen Fehler zurückgibt (404, 500 usw.), wirft fetch nicht automatisch eine Ausnahme. Wir müssen den Status manuell behandeln.
Für POST-Anfragen mit einem Body muss der Header Content-Type angegeben werden:
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. Erstellung eines universellen HTTP-Clients
\n\nAnstatt jedes Mal fetch von Grund auf neu zu schreiben, ist es besser, einen Wrapper zu erstellen – einen universellen HTTP-Client. Dies ermöglicht die zentrale Verwaltung der Basis-URL, Header und Fehlerbehandlung. Betrachten wir ein Beispiel auf Basis von Klassen:
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 // Für DELETE oder andere Methoden, die möglicherweise keinen Body zurückgeben\n if (response.status === 204) {\n return undefined as TResponse;\n }\n\n return response.jso