Jedes Node.js-Fetch-Tutorial zeigt dir await fetch(url) und tut anschließend so, als wäre damit alles geklärt. In der Produktion schluckt deine App dann klammheimlich einen 500er, eine Anfrage hängt 90 Sekunden lang ohne Timeout, und du verbringst deinen Freitagabend mit dem Debuggen von etwas, das eigentlich sofort hätte auffallen müssen.
Ich baue bei Thunderbit seit geraumer Zeit interne Tools und Datenpipelines, und eines kann ich dir sagen: Genau in der Lücke zwischen „fetch funktioniert im Tutorial“ und „fetch funktioniert in der Produktion“ entsteht der meiste Ärger. Ein Entwickler hat es auf Reddit auf den Punkt gebracht: „Wenn du in die Produktion gehst, merkst du, dass du etwas Robusteres brauchst als das native fetch.“
Ein anderer gestand: „3 Jahre lang als Webentwickler gearbeitet, und heute erst gelernt, dass der catch-Block der fetch API NICHT für HTTP-Fehler ist.“ Dieser Leitfaden nimmt sich die fünf Dinge vor, die die meisten Tutorials einfach unter den Tisch fallen lassen — die Fehlerfalle, Timeouts mit AbortController, Retry-Logik, das Wiederverwenden von Verbindungen und den Punkt, an dem du für strukturierte Datenerfassung über fetch hinausgehen solltest. Wenn dir schon mal ein fetch-Aufruf in der Produktion stillschweigend fehlgeschlagen ist, ist dieser Artikel für dich.

Was ist die Node.js Fetch API?
Die Node.js Fetch API ist die eingebaute, browserkompatible Möglichkeit, HTTP-Anfragen (GET, POST, PUT, DELETE usw.) direkt aus Node.js heraus zu senden — ohne Axios, node-fetch oder ein anderes Paket installieren zu müssen. Wenn du fetch() im Browser schon eingesetzt hast, kennst du die Syntax bereits. Dieselbe API läuft jetzt eben auch auf dem Server.
Kurz die Versionsgeschichte:
| Meilenstein | Node-Version | Was passiert ist |
|---|---|---|
| Experimentelles Fetch-Flag | v17.5.0 / v16.15.0 | fetch hinter --experimental-fetch hinzugefügt |
| Globales Fetch als Standard | v18.0.0 | Experimentelles fetch global verfügbar, betrieben von Undici |
| Stabiles Fetch | v21.0.0 | Nicht mehr experimentell |
| Produktions-Basis 2026 | v22 LTS / v24 LTS | Für die Produktion empfohlen; v20 ist jetzt EOL |
Unter der Haube läuft Node-Fetch über Undici — einen leistungsstarken HTTP-Client, der eigens für Node.js entwickelt wurde. Er setzt nicht auf das ältere eingebaute http-Modul auf. Der praktische Nutzen: Du bekommst eine moderne, Promise-basierte HTTP-API, die in deinem Browser-Code, deinem Express-Backend, deiner serverlosen Funktion und deinen CLI-Skripten gleichermaßen funktioniert.
Warum die Node.js Fetch API für deine Projekte wichtig ist
Vor Node 18 begann jedes neue Projekt mit demselben Ritual: npm install axios oder npm install node-fetch. 2026 brauchst du in einer aktuellen Node-LTS-Version für einfache HTTP-Anfragen keine zusätzliche Abhängigkeit mehr. Das zahlt sich gleich mehrfach aus: bei der Bundle-Größe, bei der Sicherheit in der Lieferkette und beim Onboarding (Front-End- und Back-End-Entwickler nutzen endlich dieselbe API).
Hier spielt natives fetch seine Stärken aus:
| Szenario | Warum natives fetch gut funktioniert | Produktions-Hinweis |
|---|---|---|
| Express/Fastify-Backend, das REST-APIs aufruft | Vertrautes async/await, keine Abhängigkeit | Timeout und response.ok-Prüfung ergänzen |
| Serverless-Funktionen (Lambda, Vercel usw.) | Kleine Cold-Start-Fläche, keine Paketinstallation | Timeout unter der maximalen Laufzeit der Plattform halten |
| CLI-Skripte und Automatisierungen | Einfaches GET/POST ohne Projekt-Setup | Retry/Backoff für instabile APIs hinzufügen |
| Webhook-Zustellung oder -Weiterleitung | Standard-HTTP-Methoden und -Header | Nicht blind nicht-idempotente POSTs erneut versuchen |
| Berichte und Dashboards | Gut für JSON-Abfragen von APIs | Für Schleifen Paging und Connection Pooling verwenden |
| Microservice-Kommunikation | Funktioniert für einfache interne HTTP-Aufrufe | Für Retry, Hooks oder HTTP/2 eher Got oder direkt Undici erwägen |
Für neue Node-22+-Projekte ist natives fetch der vernünftige Standard — es sei denn, du weißt, dass du Funktionen brauchst, die es nicht mitbringt (Interceptor, eingebautes Retry, HTTP/2 usw.). Die npm-Downloadzahlen zeigen einen Markt im Umbruch: node-fetch kommt nach wie vor auf rund 144,9 Mio. wöchentliche Downloads, ein großer Teil davon entfällt aber auf Legacy-Code und transitive Abhängigkeiten. Axios liegt bei rund 108,6 Mio., Undici bei rund 106 Mio., Got bei rund 36 Mio. und Ky bei rund 5,6 Mio.. Die Richtung ist eindeutig: Natives fetch ist die neue Grundlage, Drittanbieter-Clients sind für die Sonderfälle da.
Natives Fetch vs. node-fetch vs. Axios vs. Got vs. Ky: Die Entscheidungs-Matrix 2026
Die Frage, die mir in Entwicklerforen am häufigsten begegnet: „Welchen HTTP-Client sollte ich in Node.js verwenden?“ Ein Reddit-Nutzer brachte es so auf den Punkt: „Warum sollte ich eine Bibliothek importieren … wenn die Sprache/das Framework die Funktion schon eingebaut hat?“ Ein berechtigter Einwand — aber die Antwort hängt davon ab, was du genau brauchst.

| Funktion | Natives fetch | node-fetch v3 | axios | got v15 | ky v2 |
|---|---|---|---|---|---|
| Node.js-Version | ≥18 (empfohlen 22/24 LTS) | ≥12.20 | Breite Unterstützung | ≥22 | ≥22 |
| Installation erforderlich | Nein | Ja | Ja | Ja | Ja |
| ESM- und CJS-Unterstützung | Beide (global) | Nur ESM (v3) | Beide | Nur ESM | Nur ESM |
| Automatischer Reject bei 4xx/5xx | Nein | Nein | Ja | Ja | Ja |
| Eingebautes Retry | Nein | Nein | Nein | Ja | Ja |
| Request-Interceptor | Nein | Nein | Ja | Ja (Hooks) | Ja (Hooks) |
| Streaming-Unterstützung | Web ReadableStream | Ja | Eingeschränkt | Starke Node-Streams | Fetch-basiert |
| Bundle-/Installationsumfang | 0 KB | ~107 KB, 3 Abhängigkeiten | ~2,8 MB, 4 Abhängigkeiten | ~355 KB, 12 Abhängigkeiten | ~405 KB, 0 Abhängigkeiten |
| HTTP/2-Unterstützung | Über Undici-Dispatcher | Nein | Nein | Ja | Nein (Fetch-Wrapper) |
Eine kurze Anmerkung zum ESM/CJS-Thema: node-fetch v3 ist reines ESM, was viele Projekte mit require() lahmgelegt hat. Natives fetch ist global verfügbar — es funktioniert in CJS- und ESM-Dateien ohne Import-Verrenkungen. Wenn du wegen CommonJS noch an node-fetch v2 hängst, räumt natives fetch dieses Problem komplett aus dem Weg.
Und zu den frühen Stabilitätsbedenken: Ja, die erste fetch-Implementierung in Node 18 hatte echte Fehler. Ein Entwickler erwähnte auf Reddit: „Ich hatte kürzlich einen wilden Bug mit dem nativen Fetch in Node 18, also mussten wir unsere App umstellen.“ Das war 2023. 2026, mit Node 22 und 24 LTS, sind diese Probleme erledigt. Natives fetch ist produktionsreif.
Wann du bei nativem Fetch bleiben solltest
Bleib bei nativem fetch, wenn:
- Dein Projekt auf Node 22 LTS oder Node 24 LTS läuft.
- Es sich um einfache REST-Aufrufe handelt (GET, POST, PUT, DELETE).
- Du bereit bist, einen kleinen Wrapper für
response.ok, JSON-Parsing, Timeouts und Retry zu ergänzen. - Du keine Abhängigkeiten und weniger Risiken in der Lieferkette willst.
- Dir die Parität zwischen Browser- und Server-API wichtig ist.
- Du in serverlosen oder Edge-Umgebungen arbeitest, in denen eingebaute APIs bevorzugt werden.
Wann Axios, Got oder Ky sinnvoller sind
Axios ist die richtige Wahl, wenn dein Team auf Request-/Response-Interceptor angewiesen ist (etwa automatisches Erneuern von Auth-Tokens, Mandanten-Header, zentrales Logging), wenn du bei HTTP-Fehlern standardmäßig einen Reject möchtest oder wenn du Rückwärtskompatibilität mit älteren Node-Runtimes brauchst.
Got ist für Node-Services mit hohem Durchsatz gemacht, die eingebaute Retries, Hooks, fortgeschrittene Timeout-Phasen, Streams, Paging-Helfer, Unix-Sockets, Proxy-/Caching-Workflows oder HTTP/2-Unterstützung brauchen. Es ist das Allzweckwerkzeug für HTTP-Arbeit, die ausschließlich in Node läuft.
Ky ist die passende Wahl, wenn dir die Schlichtheit von fetch gefällt, du aber weniger Boilerplate willst — es bringt Retry, Timeout, Hooks und HTTPError in einem winzigen Paket ohne Abhängigkeiten mit.
So sendest du GET-Anfragen mit der Node.js Fetch API
Eine GET-Anfrage mit async/await sieht so aus:
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const post = await response.json();
console.log(post.title);
// → "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
Und die Variante mit .then(), falls du sie lieber magst:
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(post => console.log(post.title))
.catch(error => console.error(error));
Beides funktioniert. Aber beides ist noch nicht produktionssicher (dazu gleich mehr).
Response-Reader, die du kennen solltest:
| Methode | Wann verwenden |
|---|---|
response.json() | Der Server liefert JSON |
response.text() | Der Server liefert HTML, Klartext, CSV oder Markdown |
response.arrayBuffer() | Du brauchst Binärdaten (Bilder, Dateien) |
response.body | Du brauchst Streaming-/Chunk-Verarbeitung |
Ein besseres Muster — eines, das tatsächlich auf Fehler prüft:
async function getPost(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
return response.json();
}
const post = await getPost(1);
console.log(post.title);
Diese eine Zeile if (!response.ok) ist der Unterschied zwischen Tutorial-Code und Produktionscode. Und genau hier lauert die größte Falle.
So sendest du POST-Anfragen mit der Node.js Fetch API
POST-Anfragen folgen derselben Struktur — du setzt lediglich Methode, Header und Body:
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Leitfaden für Node-Fetch',
body: 'Produktionsreifes Fetch braucht Fehlerbehandlung.',
userId: 1,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const created = await response.json();
console.log(created.id); // → 101
Andere Request-Typen senden (PUT, DELETE, PATCH)
PUT, PATCH und DELETE verwenden dieselbe Struktur, nur mit einem anderen method-Wert:
// PUT — vollständige Ersetzung
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1, title: 'Ersetzt', body: 'Vollständige Ersetzung', userId: 1 }),
});
// PATCH — Teilaktualisierung
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Teilaktualisierung' }),
});
// DELETE
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'DELETE',
});
Die Body-Parser-Falle in Express: Wenn du JSON an einen Express-Server schickst und req.body als undefined zurückkommt, lautet die Lösung fast immer: Verwende express.json() statt express.urlencoded(). Der Server braucht die express.json()-Middleware vor deiner Route, um Content-Type: application/json-Bodies zu parsen. Das gehört zu den häufigsten Stack-Overflow-Fragen rund um Express — und es erwischt die Leute jedes Mal aufs Neue.
import express from 'express';
const app = express();
app.use(express.json()); // ← Das brauchst du für JSON-POST-Bodies
app.post('/api/posts', (req, res) => {
res.json({ received: req.body });
});
Die fetch()-Fehlerfalle, an der Produktions-Apps scheitern

Hier entstehen die meisten Fetch-Bugs in der Produktion.
fetch() lehnt sein Promise bei HTTP-4xx- oder 5xx-Fehlern nicht ab. Es lehnt nur bei Fehlern auf Netzwerkebene ab — DNS-Fehler, fehlende Internetverbindung, abgebrochene Anfragen. Gibt der Server einen 403 Forbidden oder einen 500 Internal Server Error zurück, wertet fetch das als erfolgreiche Antwort. Dein .catch()-Block wird nie ausgeführt. Dein try/catch greift nie. Dein Code verarbeitet munter weiter, was auch immer der Server zurückgeschickt hat.
Die MDN-Dokumentation sagt das ganz unmissverständlich, doch die meisten Tutorials huschen darüber hinweg. Das Ergebnis? Code wie dieser sieht sauber aus, schluckt Fehler aber klammheimlich:
try {
const response = await fetch('https://api.example.com/private');
const data = await response.json(); // ← Das läuft sogar bei einem 403
console.log('Sieht erfolgreich aus:', data);
} catch (error) {
// Nur Fehler auf Netzwerkebene landen hier
console.error('Abgefangen:', error);
}
Hier eine kurze Aufschlüsselung, was die einzelnen Muster tatsächlich abfangen:
| Muster | Fängt Netzwerkfehler ab | Fängt 4xx/5xx ab | Parst JSON sicher | Wiederverwendbar |
|---|---|---|---|---|
Rohes .then(res => res.json()) | Ja (über .catch()) | Nein | Keine Content-Type-Prüfung | Nein |
try/catch mit await fetch() | Ja | Nein | Keine Content-Type-Prüfung | Nein |
Manuelles if (!res.ok) pro Aufruf | Ja | Ja | Hängt vom jeweiligen Aufruf ab | Teilweise |
Eigener fetchJSON()-Wrapper | Ja | Ja | Ja | Ja |
Einen wiederverwendbaren fetchJSON()-Wrapper bauen
Schreib einen Wrapper. Importiere ihn überall. Und hör auf, if (!response.ok) in jede Datei zu kopieren:
export class HTTPError extends Error {
constructor(message, { status, statusText, url, body }) {
super(message);
this.name = 'HTTPError';
this.status = status;
this.statusText = statusText;
this.url = url;
this.body = body;
}
}
export async function fetchJSON(url, options = {}) {
const response = await fetch(url, {
headers: {
Accept: 'application/json',
...options.headers,
},
...options,
});
const contentType = response.headers.get('content-type') || '';
const isJSON = contentType.includes('application/json');
const body = isJSON ? await response.json().catch(() => null) : await response.text();
if (!response.ok) {
throw new HTTPError(`HTTP ${response.status} ${response.statusText}`, {
status: response.status,
statusText: response.statusText,
url: response.url,
body,
});
}
return body;
}
Gibt der Server nun einen 403 zurück:
try {
const data = await fetchJSON('https://api.example.com/private');
} catch (error) {
if (error instanceof HTTPError) {
console.error(`Server gab ${error.status} zurück:`, error.body);
} else {
console.error('Netzwerk- oder anderer Fehler:', error);
}
}
Der Fehler trägt Statuscode, Response-Body und URL mit sich — also alles, was du für Logging, Alarme oder Meldungen an Nutzer brauchst. Einmal importieren, überall verwenden.
AbortController und Timeouts: Das Produktionsmuster für die Node.js Fetch API

Ohne Timeout hängt ein fetch-Aufruf endlos, sobald der entfernte Server nicht mehr antwortet. Deine Express-Route blockiert. Deine Lambda-Funktion verheizt ihr Laufzeitbudget. Dein Skript sitzt einfach da … und wartet.
Ich habe die Top-Suchergebnisse durchgesehen: Nicht ein einziges Node.js-spezifisches Fetch-Tutorial behandelt Request-Abbruch oder Timeouts. Dabei sind Timeouts einer der Hauptgründe, warum Entwickler bei Axios oder Got bleiben. Ein Reddit-Thread trägt wörtlich den Titel „Node fetch does not timeout“.
AbortSignal.timeout() verwenden (Node 18.11+)
Der einfachste Ansatz — eine zusätzliche Option:
try {
const response = await fetch('https://api.example.com/data', {
signal: AbortSignal.timeout(5000), // 5 Sekunden
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Die Anfrage hat nach 5 Sekunden ein Timeout erreicht.');
} else {
throw error;
}
}
Achtung: AbortSignal.timeout() löst einen TimeoutError aus, keinen AbortError. Dieses Detail bekommen sogar manche erfahrenen Entwickler falsch hin.
Manuelles Timeout mit AbortController
Für mehr Kontrolle — oder wenn du eine Anfrage nicht nur per Timer, sondern auch durch eine Benutzeraktion abbrechen musst:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.example.com/data', {
signal: controller.signal,
});
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'AbortError') {
console.error('Die Anfrage wurde manuell abgebrochen.');
} else {
throw error;
}
} finally {
clearTimeout(timeout);
}
AbortError vs. TimeoutError behandeln
Diese Unterscheidung ist fürs Logging und für Meldungen an Nutzer entscheidend:
| Abbruchpfad | Fehlername im catch-Block |
|---|---|
AbortSignal.timeout(ms) | TimeoutError |
controller.abort() | AbortError |
| DNS-/Netzwerkfehler | Typischerweise TypeError: fetch failed |
Ein praxisnahes Szenario — eine Express-Route, die eine externe API aufruft und innerhalb von 3 Sekunden antworten muss:
app.get('/dashboard', async (req, res, next) => {
try {
const data = await fetchJSON('https://api.example.com/report', {
signal: AbortSignal.timeout(3000),
});
res.json(data);
} catch (error) {
if (error.name === 'TimeoutError') {
res.status(504).json({ error: 'Upstream-API hat ein Timeout erreicht' });
return;
}
next(error);
}
});
Ohne dieses Muster würde eine träge Upstream-API deine komplette Route blockieren, bis der Client aufgibt.
Retry-Logik und Connection Reuse: So machst du die Node.js Fetch API produktionsreif
Natives fetch kennt kein eingebautes Retry. Ein kurzer Netzwerkaussetzer oder ein vorübergehender 503 bedeutet: Die Anfrage schlägt einfach fehl. Für die meisten Leseoperationen in der Produktion ist das nicht hinnehmbar.
Ein zusammensetzbarer Retry-Wrapper mit exponentiellem Backoff
Das ist bewusst kurz gehalten — etwa 10 Zeilen echte Logik:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
export async function fetchWithRetry(url, options = {}, retries = 2) {
for (let attempt = 0; ; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok || ![408, 429, 500, 502, 503, 504].includes(response.status)) {
return response;
}
if (attempt >= retries) return response;
} catch (error) {
if (attempt >= retries) throw error;
}
await wait(250 * 2 ** attempt); // 250 ms, 500 ms, 1000 ms...
}
}
Wann du retryen solltest — und wann nicht
- Retryen: Idempotente GET- und HEAD-Anfragen, vorübergehende Statuscodes (408, 429, 500, 502, 503, 504), kurze Netzwerkaussetzer.
- Nicht retryen: Nicht-idempotente POST-Anfragen, die Datensätze anlegen, Geld abbuchen oder Nebenwirkungen auslösen — es sei denn, du arbeitest mit Idempotency Keys.
- Retry-After beachten: Bei 429 (Rate Limit) und 503 (Service nicht verfügbar) solltest du den
Retry-After-Header prüfen, bevor du mit dem Backoff loslegst.
Wenn du deine Retry-Logik nicht selbst schreiben willst: Ky ist ein leichtgewichtiges Fetch-Wrapper-Paket, das Retry, Timeout, Hooks und HTTPError direkt mitbringt — ganz ohne Abhängigkeiten.
Connection Reuse mit Undicis Agent und Pool
Bei Schleifen mit hohem Durchsatz — Hunderte Seiten scrapen, eine API im Batch aufrufen, einen Dienst pollen — spart das Wiederverwenden von TCP-Verbindungen jede Menge Zeit. Jede neue Verbindung bedeutet einen frischen DNS-Lookup, einen TCP-Handshake und bei HTTPS zusätzlich eine TLS-Verhandlung.
Da Node-Fetch über Undici läuft, kannst du einen eigenen Dispatcher übergeben:
import { Agent } from 'undici';
const agent = new Agent({
keepAliveTimeout: 10_000,
keepAliveMaxTimeout: 60_000,
});
const response = await fetch('https://api.example.com/data', {
dispatcher: agent,
});
Für noch mehr Kontrolle bei einem bestimmten Origin:
import { Pool } from 'undici';
const pool = new Pool('https://api.example.com', { connections: 10 });
const response = await fetch('https://api.example.com/data', {
dispatcher: pool,
});
// Wenn fertig:
await pool.close();
Die Benchmarks in der Undici-README zeigen, dass Connection Reuse und Pooling den Durchsatz drastisch steigern können — undici - dispatch lag in ihrem lokalen Benchmark bei etwa 22.234 req/s gegenüber etwa 5.904 req/s bei undici - fetch. Reale Werte schwanken, doch die Richtung ist klar: Wer viele Anfragen an denselben Origin schickt, kommt um Pooling nicht herum.
Noch ein Hinweis: Response-Bodies immer konsumieren oder abbrechen. Nicht konsumierte Bodies können in den HTTP-Interna von Node zu Ressourcenlecks führen.
Streaming-Antworten mit der Node.js Fetch API
Große Datei-Downloads, in Chunks gelieferte JSON-Feeds, Server-Sent Events, LLM-Ausgaben — in all diesen Fällen ist es Verschwendung von Zeit und Speicher, erst die komplette Antwort abzuwarten. Streaming erlaubt dir, Daten zu verarbeiten, sobald sie eintreffen.

Node 18+ bringt einen browserkompatiblen ReadableStream mit. So streamst du eine newline-delimited-JSON-Antwort und verarbeitest jede Zeile direkt beim Eintreffen:
const response = await fetch('https://example.com/large-file.ndjson');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (line) {
const item = JSON.parse(line);
console.log('Verarbeitet:', item.id);
}
}
}
Für einfaches Text-Streaming (etwa LLM-Ausgabe an stdout durchreichen):
const response = await fetch('https://example.com/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value, { stream: true }));
}
Streaming ist ein Bereich, in dem sowohl natives fetch als auch Got glänzen. Die Streaming-Unterstützung von Axios ist hier deutlich eingeschränkter.
Wenn fetch() an seine Grenzen stößt: Strukturiertes Web Scraping mit APIs
Irgendwann ist nicht mehr fetch der Engpass. Das eigentliche Problem heißt dann: „Ich habe HTML — und jetzt?“

Fetch ist ein HTTP-Client — er holt Bytes, Text, JSON oder HTML. Von Produktkarten, Preisen, Bewertungen oder Kontakttabellen weiß er nichts. Für strukturiertes Web Scraping sieht der typische rohe Stack so aus:
fetch()zum Herunterladen von HTML- Cheerio (oder Ähnliches), um Elemente mit CSS-Selektoren auszuwählen
- Eigene Paginierungslogik
- JavaScript-Rendering, wenn Seiten clientseitig gerendert werden
- Proxy-/Anti-Bot-/CAPTCHA-Behandlung
- Pflege der Selektoren, sobald sich das Layout der Website ändert
Hier ein typisches Fetch+Cheerio-Beispiel — etwa 15 Zeilen, um Produkttitel zu scrapen:
import * as cheerio from 'cheerio';
const response = await fetch('https://example-store.com/products');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const $ = cheerio.load(html);
const products = $('.product-card')
.map((_, el) => ({
name: $(el).find('.product-title').text().trim(),
price: $(el).find('.price').text().trim(),
url: new URL($(el).find('a').attr('href'), response.url).href,
}))
.get();
console.log(products);
Für stabile Seiten mit vorhersehbarem HTML funktioniert das. Es wird aber rasch fragil — clientseitig gerenderte Inhalte, wechselnde Klassennamen, Anti-Bot-Maßnahmen und Paginierung treiben die Komplexität in die Höhe.
Thunderbits Open API: Von rohem HTML zu strukturierten Daten in einem Aufruf
Hier wird ein anderes Werkzeug nützlich. Bei Thunderbit haben wir eine API-Schicht gebaut, die die unangenehmen Teile übernimmt — JavaScript-Rendering, Anti-Bot-Schutz, Layout-Änderungen —, damit du dich auf die Daten konzentrieren kannst, die dich wirklich interessieren.
Distill API (POST /distill): Verwandelt jede URL in sauberes Markdown. Praktisch für LLMs, Wissensdatenbanken oder Inhaltsanalysen — kein HTML-Parser nötig.
Extract API (POST /extract): Du definierst ein JSON-Schema für die strukturierten Daten, die du haben willst (Produktname, Preis, Bewertung), und die KI extrahiert sie. Keine CSS-Selektoren, kein Bruch bei Layout-Änderungen.
Hier dieselbe Produkt-Scraping-Aufgabe mit Thunderbits Extract API — aufgerufen über natives fetch:
const response = await fetch('https://openapi.thunderbit.com/openapi/v1/extract', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.THUNDERBIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example-store.com/products',
renderMode: 'basic',
schema: {
type: 'object',
properties: {
products: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Produktname' },
price: { type: 'string', description: 'Angezeigter Produktpreis' },
rating: { type: 'number', description: 'Durchschnittliche Kundenbewertung' },
},
required: ['name', 'price'],
},
},
},
required: ['products'],
},
}),
});
if (!response.ok) throw new Error(`Thunderbit API: ${response.status}`);
const result = await response.json();
console.log(result.data);
Der Vergleich: etwa 15 Zeilen Fetch+Cheerio (plus fragile Selektoren) gegenüber einem einzigen API-Aufruf, der sauberes JSON liefert. Für Batch-Jobs verarbeitet Thunderbit bis zu 50 URLs pro Batch-Extract-Aufruf und bis zu 100 URLs pro Batch-Distill-Aufruf.
Thunderbit ersetzt fetch nicht — fetch ist der Transport. Thunderbit ist die Extraktionsschicht, zu der du greifst, wenn rohes HTML-Parsen zum eigentlichen Problem wird. Falls dich die Preise interessieren: Die kostenlose Stufe gibt dir 600 API-Einheiten zum Ausprobieren, bezahlte Pläne beginnen bei 6 $/Monat (ca. 5,50 €/Monat). Du kannst dir auch die Thunderbit Chrome-Erweiterung ansehen, um ganz ohne Code direkt im Browser zu extrahieren.
Mehr zu strukturierten Scraping-Ansätzen findest du in unseren Leitfäden zu den besten Data-Extraction-Tools, wie man einen Web-Scraper erstellt und Daten von einer Website nach Excel scrapen, die konkrete Workflows im Detail durchgehen.
Schnellreferenz: Node.js Fetch API Spickzettel
Diesen Abschnitt solltest du dir als Lesezeichen setzen. Komm zurück, wenn du ein Muster zum Kopieren und Einfügen brauchst.
| Muster | Snippet |
|---|---|
| Einfaches GET | const res = await fetch(url); const data = await res.json(); |
| Einfaches POST | await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); |
| HTTP-Fehlerprüfung | if (!res.ok) throw new Error(\\HTTP ${res.status}\); |
| Timeout (einfach) | await fetch(url, { signal: AbortSignal.timeout(5000) }); |
| Manueller Abbruch | const c = new AbortController(); setTimeout(() => c.abort(), 5000); await fetch(url, { signal: c.signal }); |
| Retry-Statuscodes | 408, 429, 500, 502, 503, 504 erneut versuchen. POST nicht blind retryen. |
| JSON-Wrapper | Verwende fetchJSON(), um ok zu prüfen, den Content-Type zu parsen und HTTPError auszulösen. |
| Connection Pool | import { Pool } from 'undici'; const pool = new Pool(origin, { connections: 10 }); fetch(url, { dispatcher: pool }); |
| Stream-Chunks | const reader = res.body.getReader(); in einer Schleife await reader.read() |
| Strukturierte Extraktion | Verwende die Thunderbit Extract API, wenn es um Felder von einer Webseite geht und nicht um rohes HTML. |
Fazit und wichtigste Erkenntnisse
Natives fetch in Node.js ist 2026 produktionsreif — kein node-fetch mehr für neue Projekte, keine Standard-Axios-Abhängigkeit erforderlich. Aber reines fetch() allein ist noch keine produktionsreife HTTP-Strategie.
Die fünf Dinge, die die meisten Tutorials auslassen — und die dieser Leitfaden abdeckt:
- Die Fehlerfalle:
fetch()wirft bei 4xx/5xx nicht. Prüfe immerresponse.okoder nutze einen Wrapper wiefetchJSON(). - Timeouts: Verwende
AbortSignal.timeout()für die einfachen Fälle.AbortSignal.timeout()wirftTimeoutError; ein manuellescontroller.abort()wirftAbortError. - Retry-Logik: Nicht eingebaut. Ergänze exponentielles Backoff für idempotente Anfragen und vorübergehende Fehler. Oder nimm Ky für fetch-ähnliches Retry direkt ab Werk.
- Connection Reuse: Bei Schleifen mit hohem Durchsatz nutze Undicis
AgentoderPoolüber die Optiondispatcher. - Strukturierte Extraktion: Wenn du Daten aus Webseiten brauchst und nicht nur rohes HTML, erwäge eine Extraktions-API wie Thunderbit statt fragiler CSS-Selektoren.
Die Entscheidungs-Matrix in einem Satz: Nimm natives fetch für die meisten Projekte, Axios für Interceptor, Got für eingebautes Retry und HTTP/2, Ky für fetch mit besseren Standardwerten und Thunderbits API, sobald deine fetch-basierten Scraping-Skripte zu komplex werden, um sie noch wartbar zu halten.
Thunderbit für strukturierte Datenerfassung testen
Probier die Muster aus diesem Leitfaden aus. Und wenn du sehen willst, wie Thunderbit strukturierte Extraktion handhabt, ist die kostenlose Stufe ein guter Startpunkt — oder schau dir eine Einführung auf dem Thunderbit-YouTube-Kanal an.
Thunderbit für KI-Web-Scraping testen Get Started Free
FAQs
1. Ist fetch in Node.js eingebaut oder muss ich es installieren?
Fetch ist ab Node.js 18 eingebaut — keine Installation nötig. Es wurde in Node 21 stabil und wird in Node 22 LTS und Node 24 LTS voll unterstützt. Für ältere Node-Versionen kannst du das npm-Paket node-fetch nutzen, doch neue Projekte sollten auf eine gepflegte LTS-Version setzen.
2. Wirft fetch bei 404- oder 500-Antworten einen Fehler?
Nein. Fetch lehnt sein Promise nur bei Fehlern auf Netzwerkebene ab (DNS-Fehler, keine Verbindung, abgebrochene Anfragen). HTTP-Antworten wie 404, 403 und 500 werden ganz normal aufgelöst, mit response.ok === false. Du musst response.ok oder response.status selbst prüfen — oder einen Wrapper wie die in diesem Leitfaden gezeigte Funktion fetchJSON() verwenden.
3. Wie füge ich in Node.js ein Timeout zu fetch hinzu?
Am einfachsten geht es mit AbortSignal.timeout(ms), verfügbar ab Node 18.11+: await fetch(url, { signal: AbortSignal.timeout(5000) }). Das wirft einen TimeoutError, wenn die Anfrage länger als 5 Sekunden dauert. Für mehr Kontrolle erstellst du manuell einen AbortController und rufst controller.abort() aus einem setTimeout heraus auf. Fange AbortError beim manuellen Muster und TimeoutError bei AbortSignal.timeout() ab.
4. Kann ich fetch für Web Scraping in Node.js verwenden?
Ja, aber fetch liefert nur rohes HTML. Du brauchst einen Parser wie Cheerio, um bestimmte Elemente zu extrahieren, plus eigene Logik für Paginierung, clientseitig gerenderte Seiten und Anti-Bot-Maßnahmen. Für strukturierte Datenerfassung in großem Maßstab — wenn du sauberes JSON mit Produktnamen, Preisen oder Kontaktdaten willst — solltest du Thunderbits Extract API in Betracht ziehen, die mit KI strukturierte Daten ganz ohne CSS-Selektoren oder layoutabhängigen Code zurückgibt.
5. Sollte ich 2026 von Axios auf natives fetch umsteigen?
Für neue Projekte auf Node 22+ ist natives fetch eine starke Standardwahl. Es kommt ohne Abhängigkeiten aus, basiert auf Promises und nutzt dieselbe API wie das Browser-Fetch. Behalte Axios, wenn du auf Request-/Response-Interceptor, standardmäßige HTTP-Fehler-Rejektion oder Rückwärtskompatibilität mit älteren Node-Versionen angewiesen bist. Beide Wege sind valide — die Entscheidung hängt davon ab, welche Funktionen dein Projekt tatsächlich nutzt.
Mehr erfahren


