Każdy poradnik o fetch w Node.js uczy Cię await fetch(url) i na tym kończy temat. Potem Twoja produkcyjna aplikacja po cichu zjada błąd 500, żądanie wisi 90 sekund bez timeoutu, a Ty spędzasz piątkowy wieczór na debugowaniu czegoś, co powinno być oczywiste.
Od jakiegoś czasu buduję wewnętrzne narzędzia i pipeline'y danych w Thunderbit i mogę powiedzieć jedno: przepaść między „fetch działa w tutorialu” a „fetch działa w produkcji” to właśnie miejsce, gdzie kryje się większość problemów. Jeden deweloper na Reddicie ujął to idealnie: „kiedy wchodzisz do produkcji, okazuje się, że potrzebujesz czegoś bardziej odpornego niż natywny fetch.”
Inny przyznał: „Pracowałem 3 lata jako web developer i dopiero dziś się dowiedziałem, że blok catch w Fetch API NIE służy do błędów HTTP.” Ten przewodnik omawia pięć rzeczy, które większość tutoriali pomija — pułapkę błędów, timeouty z AbortController, logikę ponowień, ponowne użycie połączeń oraz moment, w którym warto wyjść poza fetch przy strukturalnej ekstrakcji danych. Jeśli kiedykolwiek fetch zawiódł Cię po cichu w produkcji, ten tekst jest dla Ciebie.

Czym jest Node.js Fetch API?
Node.js Fetch API to wbudowany, zgodny z przeglądarką sposób wykonywania żądań HTTP (GET, POST, PUT, DELETE itd.) z poziomu Node.js — bez instalowania Axios, node-fetch ani żadnego innego pakietu. Jeśli używałeś fetch() w przeglądarce, składnię już znasz. Teraz to samo API działa też po stronie serwera.
Krótka historia wersji:
| Kamień milowy | Wersja Node | Co się wydarzyło |
|---|---|---|
| Eksperymentalna flaga fetch | v17.5.0 / v16.15.0 | fetch dodany za flagą --experimental-fetch |
| Domyślny globalny fetch | v18.0.0 | Eksperymentalny fetch dostępny globalnie, napędzany przez Undici |
| Stabilny fetch | v21.0.0 | Nie jest już eksperymentalny |
| Baza produkcyjna na 2026 | v22 LTS / v24 LTS | Zalecane do produkcji; v20 jest już EOL |
Pod spodem fetch w Node działa dzięki Undici — wydajnemu klientowi HTTP zbudowanemu specjalnie dla Node.js. Nie opiera się na starszym, wbudowanym module http. Praktyczna korzyść: dostajesz nowoczesne, oparte na Promise API HTTP, które działa tak samo w kodzie przeglądarkowym, backendzie Express, funkcji serverless i skryptach CLI.
Dlaczego Node.js Fetch API ma znaczenie w Twoich projektach
Przed Node 18 każdy nowy projekt zaczynał się od tego samego rytuału: npm install axios albo npm install node-fetch. W 2026 roku, jeśli projekt działa na utrzymywanym LTS Node, podstawowe żądania HTTP nie wymagają żadnych zależności. To realny plus dla rozmiaru bundle'a, bezpieczeństwa łańcucha dostaw i onboardingu — front-end i back-end wreszcie korzystają z tego samego API.
Tam, gdzie natywny fetch błyszczy:
| Scenariusz | Dlaczego natywny fetch działa dobrze | Ograniczenie w produkcji |
|---|---|---|
| Backend Express/Fastify wywołujący REST API | Znane async/await, bez zależności | Dodaj timeout i sprawdzanie response.ok |
| Funkcje serverless (Lambda, Vercel itd.) | Mały narzut na cold start, bez instalacji paczek | Utrzymuj timeout poniżej maksymalnego czasu platformy |
| Skrypty CLI i automatyzacje | Proste GET/POST bez konfiguracji projektu | Dodaj retry/backoff dla niestabilnych API |
| Dostarczanie lub przekazywanie webhooków | Standardowe metody i nagłówki HTTP | Nie ponawiaj bezmyślnie nie-idempotentnych POST-ów |
| Raporty i dashboardy | Dobre do pobierania JSON-a z API | Używaj paginacji i puli połączeń w pętlach |
| Komunikacja mikroserwisów | Działa dla prostych wewnętrznych wywołań HTTP | Rozważ Got lub bezpośrednio Undici dla retry, hooków lub HTTP/2 |
W nowych projektach na Node 22+ natywny fetch to rozsądny domyślny wybór — chyba że wiesz, że potrzebujesz funkcji, których nie oferuje (interceptory, wbudowane retry, HTTP/2 itd.). Liczby pobrań z npm pokazują, jak zmienia się rynek: node-fetch nadal notuje ok. 144,9 mln pobrań tygodniowo, ale duża część to stare i tranzytywne zależności. Axios ma ok. 108,6 mln, Undici ok. 106 mln, Got ok. 36 mln, a Ky ok. 5,6 mln. Trend jest jasny: natywny fetch to nowa baza, a klienty firm trzecich służą do konkretnych potrzeb.
Natywny fetch vs node-fetch vs Axios vs Got vs Ky: macierz decyzji na 2026 rok
Najczęstsze pytanie, które widzę na forach developerskich: „Którego klienta HTTP powinienem używać w Node.js?” Jeden użytkownik Reddita podsumował to tak: „po co importować bibliotekę… skoro język/framework ma to wbudowane?” Słuszna uwaga — ale odpowiedź zależy od tego, czego potrzebujesz.

| Funkcja | Natywny fetch | node-fetch v3 | axios | got v15 | ky v2 |
|---|---|---|---|---|---|
| Wersja Node.js | ≥18 (zalecane 22/24 LTS) | ≥12.20 | Szeroki zakres | ≥22 | ≥22 |
| Wymaga instalacji | Nie | Tak | Tak | Tak | Tak |
| Obsługa ESM + CJS | Obie (globalnie) | Tylko ESM (v3) | Obie | Tylko ESM | Tylko ESM |
| Automatyczne odrzucenie przy 4xx/5xx | Nie | Nie | Tak | Tak | Tak |
| Wbudowane retry | Nie | Nie | Nie | Tak | Tak |
| Interceptory żądań | Nie | Nie | Tak | Tak (hooks) | Tak (hooks) |
| Obsługa streamów | Web ReadableStream | Tak | Ograniczona | Silne streamy Node | Oparte na fetch |
| Rozmiar bundle'a/instalacji | 0 KB | ~107 KB, 3 zależności | ~2,8 MB, 4 zależności | ~355 KB, 12 zależności | ~405 KB, 0 zależności |
| Obsługa HTTP/2 | Przez dispatcher Undici | Nie | Nie | Tak | Nie (wrapper fetch) |
Krótka uwaga o problemie ESM/CJS: node-fetch v3 jest wyłącznie ESM, co popsuło wiele projektów używających require(). Natywny fetch jest globalny — działa zarówno w plikach CJS, jak i ESM bez żadnych kombinacji z importem. Jeśli utknąłeś na node-fetch v2 przez CommonJS, natywny fetch rozwiązuje ten problem całkowicie.
A co z początkowymi obawami o stabilność: tak, w pierwszych implementacjach fetch w Node 18 były realne błędy. Jeden deweloper na Reddicie napisał: „Niedawno złapałem dziwnego buga w natywnym fetchu z Node 18, więc musieliśmy przerobić aplikację.” To było w 2023 roku. W 2026, przy Node 22 i 24 LTS, te problemy są już rozwiązane. Natywny fetch jest gotowy do produkcji.
Kiedy zostać przy natywnym fetch
Wybierz natywny fetch, gdy:
- Twój projekt działa na Node 22 LTS lub Node 24 LTS.
- Żądania są proste i oparte na REST (GET, POST, PUT, DELETE).
- Jesteś gotów dodać mały wrapper dla
response.ok, parsowania JSON-a, timeoutów i retry. - Chcesz mieć zerowy narzut zależności i mniej obaw o łańcuch dostaw.
- Zależy Ci na zgodności API między przeglądarką a serwerem.
- Pracujesz w środowiskach serverless lub edge, gdzie preferuje się wbudowane API.
Kiedy Axios, Got albo Ky mają większy sens
Axios będzie lepszy, gdy Twój zespół polega na interceptorach request/response (np. automatyczne odświeżanie tokenu, nagłówki tenantów, scentralizowane logowanie), gdy chcesz domyślnego odrzucania przy błędach HTTP albo potrzebujesz zgodności wstecznej ze starszymi wersjami Node.
Got jest stworzony dla usług Node o dużym przepływie, które potrzebują wbudowanego retry, hooków, zaawansowanych faz timeoutu, streamów, narzędzi do paginacji, gniazd Unix, workflow proxy/caching albo obsługi HTTP/2. To taki scyzoryk szwajcarski do pracy HTTP wyłącznie w Node.
Ky to złoty środek, jeśli lubisz prostotę fetch, ale chcesz mniej boilerplate'u — dodaje retry, timeout, hooki i HTTPError w niewielkim pakiecie bez zależności.
Jak wykonać żądanie GET z Node.js Fetch API
Żądanie GET z async/await wygląda tak:
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"
A jeśli wolisz łańcuch .then():
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(post => console.log(post.title))
.catch(error => console.error(error));
Oba warianty działają. Ale żaden z nich nie jest jeszcze bezpieczny produkcyjnie (za chwilę do tego wrócimy).
Warto znać następujące metody odczytu odpowiedzi:
| Metoda | Kiedy używać |
|---|---|
response.json() | Serwer zwraca JSON |
response.text() | Serwer zwraca HTML, zwykły tekst, CSV, Markdown |
response.arrayBuffer() | Potrzebujesz danych binarnych (obrazy, pliki) |
response.body | Potrzebujesz streamingu/przetwarzania blokowego |
Lepszy wzorzec — taki, który faktycznie sprawdza błędy:
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);
Ta linia if (!response.ok) to różnica między tutorialem a kodem produkcyjnym. I tu dochodzimy do największej pułapki.
Jak wysyłać żądania POST z Node.js Fetch API
Żądania POST mają ten sam kształt — ustawiasz tylko metodę, nagłówki i body:
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Przewodnik po Node fetch',
body: 'Produkcja wymaga obsługi błędów.',
userId: 1,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const created = await response.json();
console.log(created.id); // → 101
Wysyłanie innych typów żądań (PUT, DELETE, PATCH)
PUT, PATCH i DELETE używają identycznej struktury, zmienia się tylko wartość method:
// PUT — pełne zastąpienie
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1, title: 'Zastąpiono', body: 'Pełne zastąpienie', userId: 1 }),
});
// PATCH — częściowa aktualizacja
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Częściowa aktualizacja' }),
});
// DELETE
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'DELETE',
});
Pułapka body-parser w Express: jeśli wysyłasz JSON do serwera Express, a req.body wraca jako undefined, naprawa jest niemal zawsze taka: użyj express.json(), a nie express.urlencoded(). Serwer potrzebuje middleware express.json() przed trasą, żeby sparsować body z Content-Type: application/json. To jedno z najczęstszych pytań na Stack Overflow o Express i potrafi zaskoczyć za każdym razem.
import express from 'express';
const app = express();
app.use(express.json()); // ← Tego potrzebujesz dla JSON-owych body w POST
app.post('/api/posts', (req, res) => {
res.json({ received: req.body });
});
Pułapka fetch(), która psuje aplikacje produkcyjne

To właśnie stąd bierze się większość produkcyjnych błędów z fetch.
fetch() nie odrzuca Promise przy błędach HTTP 4xx ani 5xx. Odrzuca go tylko przy awariach na poziomie sieci — błędach DNS, braku internetu, anulowanych żądaniach. Jeśli serwer zwróci 403 Forbidden albo 500 Internal Server Error, fetch uzna to za udane odpowiedź. Twój blok .catch() nigdy się nie uruchomi. Twój try/catch też tego nie przechwyci. Kod po prostu przetworzy to, co serwer odesłał.
Dokumentacja MDN mówi o tym jasno, ale większość tutoriali prześlizguje się po tym temacie. Efekt? Kod taki jak poniżej wygląda dobrze, ale po cichu zjada błędy:
try {
const response = await fetch('https://api.example.com/private');
const data = await response.json(); // ← To wykona się nawet przy 403
console.log('Wygląda na sukces:', data);
} catch (error) {
// Tylko błędy sieciowe trafiają tutaj
console.error('Przechwycono:', error);
}
Szybki podział tego, co naprawdę przechwytują poszczególne wzorce:
| Wzorzec | Łapie błędy sieciowe | Łapie 4xx/5xx | Bezpiecznie parsuje JSON | Nadaje się do ponownego użycia |
|---|---|---|---|---|
Surowe .then(res => res.json()) | Tak (przez .catch()) | Nie | Bez sprawdzania content-type | Nie |
try/catch z await fetch() | Tak | Nie | Bez sprawdzania content-type | Nie |
Ręczne if (!res.ok) przy każdym wywołaniu | Tak | Tak | Zależy od każdego wywołania | Częściowo |
Własny wrapper fetchJSON() | Tak | Tak | Tak | Tak |
Zbuduj wielokrotnego użytku wrapper fetchJSON()
Zbuduj jeden wrapper. Importuj go wszędzie. Przestań kopiować if (!response.ok) do każdego pliku:
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;
}
Teraz, gdy serwer zwróci 403:
try {
const data = await fetchJSON('https://api.example.com/private');
} catch (error) {
if (error instanceof HTTPError) {
console.error(`Serwer zwrócił ${error.status}:`, error.body);
} else {
console.error('Błąd sieciowy lub inny:', error);
}
}
Błąd zawiera kod statusu, treść odpowiedzi i URL — wszystko, czego potrzebujesz do logowania, alertów lub komunikatów dla użytkownika. Zaimportuj to raz i używaj wszędzie.
AbortController i timeouty: wzorzec produkcyjny dla Node.js Fetch API

Bez timeoutu fetch potrafi wisieć bez końca, gdy zdalny serwer przestaje odpowiadać. Trasa Express blokuje się. Lambda spala budżet wykonania. Twój skrypt po prostu... siedzi.
Sprawdziłem najpopularniejsze wyniki wyszukiwania: ani jeden poradnik fetch dla Node.js nie opisuje anulowania żądań ani timeoutów. A jednak timeouty to jeden z głównych powodów, dla których deweloperzy nadal wybierają Axios lub Got. Jeden wątek na Reddicie nosi dosłownie tytuł „Node fetch does not timeout”.
Używanie AbortSignal.timeout() (Node 18.11+)
Najprostsze podejście — jedna dodatkowa opcja:
try {
const response = await fetch('https://api.example.com/data', {
signal: AbortSignal.timeout(5000), // 5 sekund
});
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('Żądanie przekroczyło limit 5 sekund.');
} else {
throw error;
}
}
Uwaga: AbortSignal.timeout() rzuca TimeoutError, a nie AbortError. To detal, który nawet doświadczeni deweloperzy czasem mylą.
Ręczny timeout z AbortController
Jeśli chcesz mieć większą kontrolę — albo anulować żądanie na podstawie działania użytkownika, a nie tylko timera:
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('Żądanie zostało ręcznie anulowane.');
} else {
throw error;
}
} finally {
clearTimeout(timeout);
}
Obsługa AbortError vs TimeoutError
To rozróżnienie ma znaczenie przy logowaniu i komunikatach dla użytkownika:
| Ścieżka anulowania | Nazwa błędu w bloku catch |
|---|---|
AbortSignal.timeout(ms) | TimeoutError |
controller.abort() | AbortError |
| Awaria DNS/sieci | Zwykle TypeError: fetch failed |
Oto praktyczny scenariusz — trasa Express, która wywołuje zewnętrzne API i musi odpowiedzieć w ciągu 3 sekund:
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 przekroczyło limit czasu' });
return;
}
next(error);
}
});
Bez takiego wzorca wolne zewnętrzne API zablokowałoby całą trasę, dopóki klient nie odpuści.
Logika retry i ponowne użycie połączeń: jak zrobić z Node.js Fetch API narzędzie klasy produkcyjnej
Natywny fetch nie ma wbudowanego retry. Krótkotrwały problem sieciowy albo przejściowy 503 oznacza po prostu porażkę żądania. W większości odczytów produkcyjnych to nie jest akceptowalne.
Kompozycyjny wrapper retry z wykładniczym backoffem
To celowo krótkie — około 10 linii prawdziwej logiki:
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...
}
}
Kiedy ponawiać, a kiedy nie
- Ponawiaj: idempotentne żądania GET i HEAD, stany przejściowe (408, 429, 500, 502, 503, 504), chwilowe problemy sieciowe.
- Nie ponawiaj: nie-idempotentnych POST-ów, które tworzą rekordy, pobierają opłaty albo wywołują efekty uboczne — chyba że używasz kluczy idempotencji.
- Szanuj Retry-After: dla 429 (limit rate) i 503 (usługa niedostępna) sprawdź nagłówek
Retry-Afterprzed backoffem.
Jeśli nie chcesz budować własnej logiki retry, Ky to lekki wrapper fetch, który od razu dodaje retry, timeout, hooki i HTTPError — bez zależności.
Ponowne użycie połączeń z Agent i Pool w Undici
W pętlach o dużej przepustowości — scraping setek stron, wywoływanie API w batchu, polling usługi — ponowne używanie połączeń TCP oszczędza sporo czasu. Każde nowe połączenie to nowy lookup DNS, handshake TCP i — dla HTTPS — negocjacja TLS.
Ponieważ fetch w Node jest napędzany przez Undici, możesz przekazać własny dispatcher:
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,
});
Dla jeszcze większej kontroli nad konkretnym originem:
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,
});
// Gdy skończysz:
await pool.close();
Wyniki benchmarków w README Undici pokazują, że ponowne użycie połączeń i pooling mogą drastycznie poprawić throughput — undici - dispatch osiągnęło około 22 234 req/s, podczas gdy undici - fetch około 5 904 req/s w lokalnym benchmarku. Wyniki w realnym świecie będą się różnić, ale kierunek jest jasny: jeśli robisz dużo żądań do tego samego originu, pooling ma znaczenie.
Jeszcze jedna rzecz: zawsze konsumuj albo anuluj body odpowiedzi. Niekonsumowane body mogą powodować wycieki zasobów wewnątrz HTTP w Node.
Streamowanie odpowiedzi z Node.js Fetch API
Duże pobrania plików, porcjowane feedy JSON, server-sent events, output z LLM — to wszystko przypadki, w których czekanie na całą odpowiedź przed przetworzeniem marnuje czas i pamięć. Streamowanie pozwala obsługiwać dane w miarę ich napływu.

Node 18+ zawiera zgodny z przeglądarką ReadableStream. Oto jak streamować odpowiedź w formacie newline-delimited JSON i przetwarzać każdą linię po jej nadejściu:
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('Przetworzono:', item.id);
}
}
}
Dla prostszego streamowania tekstu (np. wypisywania outputu LLM do stdout):
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 to obszar, w którym natywny fetch i Got wypadają świetnie. Obsługa streamów w Axios jest bardziej ograniczona.
Gdy fetch() dochodzi do swoich granic: strukturalny web scraping przez API
W pewnym momencie fetch przestaje być wąskim gardłem. Prawdziwy problem brzmi: „mam HTML, i co dalej?”

Fetch to klient HTTP — pobiera bajty, tekst, JSON albo HTML. Nie wie nic o kartach produktu, cenie, ocenie czy tabeli kontaktów. W przypadku strukturalnego web scrapingu typowy surowy stos wygląda tak:
fetch()do pobrania HTML- Cheerio (lub podobne narzędzie) do wybierania elementów selektorami CSS
- Własna logika paginacji
- Renderowanie JavaScript, gdy strony są po stronie klienta
- Obsługa proxy / anti-bot / CAPTCHA
- Utrzymywanie selektorów za każdym razem, gdy zmieni się układ strony
Oto typowy przykład fetch + Cheerio — około 15 linii, żeby zebrać tytuły produktów:
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);
To działa na stabilnych stronach z przewidywalnym HTML-em. Szybko jednak staje się kruche — treści renderowane przez JavaScript, zmieniające się nazwy klas, zabezpieczenia anty-bot i paginacja dokładają złożoności.
Otwarte API Thunderbit: od surowego HTML do strukturalnych danych w jednym wywołaniu
Tu przydaje się inne narzędzie. W Thunderbit zbudowaliśmy warstwę API, która ogarnia brudne elementy — renderowanie JavaScript, ochronę anty-bot, zmiany układu — żebyś mógł skupić się na danych, których naprawdę potrzebujesz.
Distill API (POST /distill): zamienia dowolny URL w czysty Markdown. Przydatne do zasilania LLM-ów, budowy baz wiedzy lub analizy treści — bez parsera HTML.
Extract API (POST /extract): definiujesz JSON Schema opisujący dane strukturalne, których potrzebujesz (nazwa produktu, cena, ocena), a AI je wyciąga. Bez selektorów CSS, bez psucia się przy zmianie układu.
To samo zadanie scrapowania produktów z użyciem Thunderbit Extract API — wywołane natywnym 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: 'Nazwa produktu' },
price: { type: 'string', description: 'Wyświetlana cena produktu' },
rating: { type: 'number', description: 'Średnia ocena klientów' },
},
required: ['name', 'price'],
},
},
},
required: ['products'],
},
}),
});
if (!response.ok) throw new Error(`Thunderbit API: ${response.status}`);
const result = await response.json();
console.log(result.data);
Porównanie: około 15 linii fetch + Cheerio (plus kruche selektory) kontra jedno wywołanie API zwracające czysty JSON. W przypadku zadań wsadowych Thunderbit obsługuje do 50 URL-i na jedno wywołanie batch extract i do 100 URL-i na jedno wywołanie batch distill.
Thunderbit nie zastępuje fetch — fetch jest transportem. Thunderbit to warstwa ekstrakcji, po którą sięgasz, gdy parsowanie surowego HTML staje się prawdziwym problemem. Jeśli ciekawi Cię cennik, darmowy plan daje 600 jednostek API do eksperymentów, a płatne plany zaczynają się od 6 USD/miesiąc. Możesz też sprawdzić rozszerzenie Thunderbit do Chrome do bezkodowej ekstrakcji bezpośrednio w przeglądarce.
Więcej o strukturalnym scrapingu znajdziesz w naszych przewodnikach: najlepsze narzędzia do ekstrakcji danych, jak stworzyć web scraper oraz scraping danych ze strony do Excela, gdzie opisujemy konkretne workflowy krok po kroku.
Szybka ściąga: Node.js Fetch API cheat sheet
Ta sekcja nadaje się do zapisania w zakładkach. Wróć do niej, gdy potrzebujesz wzorca do skopiowania i wklejenia.
| Wzorzec | Fragment |
|---|---|
| Podstawowy GET | const res = await fetch(url); const data = await res.json(); |
| Podstawowy POST | await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); |
| Sprawdzanie błędu HTTP | if (!res.ok) throw new Error(\\HTTP ${res.status}\); |
| Timeout (prosty) | await fetch(url, { signal: AbortSignal.timeout(5000) }); |
| Ręczne anulowanie | const c = new AbortController(); setTimeout(() => c.abort(), 5000); await fetch(url, { signal: c.signal }); |
| Statusy do retry | Ponów 408, 429, 500, 502, 503, 504. Nie ponawiaj bezmyślnie POST. |
| Wrapper JSON | Użyj fetchJSON(), aby sprawdzić ok, sparsować content type i rzucić HTTPError. |
| Pula połączeń | import { Pool } from 'undici'; const pool = new Pool(origin, { connections: 10 }); fetch(url, { dispatcher: pool }); |
| Streamowanie bloków | const reader = res.body.getReader(); pętla po await reader.read() |
| Strukturalna ekstrakcja | Użyj Thunderbit Extract API, gdy celem są pola ze strony, a nie surowy HTML. |
Podsumowanie i najważniejsze wnioski
Natywny fetch w Node.js jest gotowy do produkcji w 2026 roku — nowych projektów nie trzeba już opierać na node-fetch, a domyślna zależność Axios nie jest potrzebna. Ale sam surowy fetch() nie jest jeszcze strategią HTTP klasy produkcyjnej.
Pięć rzeczy, które większość tutoriali pomija — a które ten przewodnik omawia:
- Pułapka błędów:
fetch()nie rzuca przy 4xx/5xx. Zawsze sprawdzajresponse.okalbo użyj wrappera typufetchJSON(). - Timeouty: do prostych przypadków używaj
AbortSignal.timeout().AbortSignal.timeout()rzucaTimeoutError; ręcznecontroller.abort()rzucaAbortError. - Logika retry: nie jest wbudowana. Dodaj exponential backoff dla żądań idempotentnych i przejściowych awarii. Albo użyj Ky, jeśli chcesz retry w stylu fetch od ręki.
- Ponowne użycie połączeń: w pętlach o dużym przepływie użyj
AgentalboPoolz Undici przez opcjędispatcher. - Strukturalna ekstrakcja: gdy potrzebujesz danych ze stron WWW, a nie tylko surowego HTML, rozważ API ekstrakcyjne takie jak Thunderbit zamiast utrzymywać kruche selektory CSS.
Macierz decyzji w jednym zdaniu: używaj natywnego fetch do większości projektów, Axios do interceptorów, Got do wbudowanego retry i HTTP/2, Ky jako fetch z lepszymi domyślnymi ustawieniami, a API Thunderbit wtedy, gdy skrypty scrapujące oparte na fetch stają się zbyt złożone w utrzymaniu.
Wypróbuj Thunderbit do ekstrakcji danych strukturalnych
Wypróbuj wzorce z tego przewodnika. A jeśli chcesz zobaczyć, jak Thunderbit radzi sobie z ekstrakcją strukturalną, darmowy plan to dobre miejsce na start — albo obejrzyj omówienie na kanale Thunderbit na YouTube.
Wypróbuj Thunderbit do AI Web Scraping Get Started Free
FAQ
1. Czy fetch jest wbudowany w Node.js, czy trzeba go instalować?
Fetch jest wbudowany w Node.js 18 i nowszym — nie trzeba nic instalować. Stał się stabilny w Node 21 i jest w pełni obsługiwany w Node 22 LTS oraz Node 24 LTS. W starszych wersjach Node można użyć pakietu npm node-fetch, ale nowe projekty powinny celować w utrzymywaną wersję LTS.
2. Czy fetch rzuca błąd przy odpowiedzi 404 albo 500?
Nie. Fetch odrzuca Promise tylko przy awariach na poziomie sieci (błędy DNS, brak połączenia, anulowane żądania). Odpowiedzi HTTP, takie jak 404, 403 i 500, rozwiązują się normalnie z response.ok === false. Musisz jawnie sprawdzać response.ok albo response.status — lub użyć wrappera, takiego jak pokazana w tym przewodniku funkcja fetchJSON().
3. Jak dodać timeout do fetch w Node.js?
Najprostsze podejście to AbortSignal.timeout(ms), dostępne od Node 18.11+: await fetch(url, { signal: AbortSignal.timeout(5000) }). Jeśli żądanie przekroczy 5 sekund, zostanie rzucony TimeoutError. Dla większej kontroli utwórz ręcznie AbortController i wywołaj controller.abort() z setTimeout. Przechwytuj AbortError dla ręcznego wzorca i TimeoutError dla AbortSignal.timeout().
4. Czy mogę używać fetch do web scrapingu w Node.js?
Tak, ale fetch zwraca tylko surowy HTML. Potrzebujesz parsera, takiego jak Cheerio, żeby wyciągać konkretne elementy, a także własnej logiki do paginacji, stron renderowanych w JavaScript i zabezpieczeń anty-bot. Przy strukturalnej ekstrakcji danych na dużą skalę — gdy chcesz czysty JSON z nazwami produktów, cenami albo danymi kontaktowymi — rozważ Thunderbit Extract API, które używa AI, aby zwracać dane strukturalne bez selektorów CSS i kodu zależnego od układu strony.
5. Czy w 2026 powinienem przejść z Axios na natywny fetch?
W nowych projektach na Node 22+ natywny fetch to bardzo mocny domyślny wybór. Jest bez zależności, oparty na Promise i ma to samo API co fetch w przeglądarce. Zostaw Axios, jeśli polegasz na interceptorach request/response, domyślnym odrzucaniu błędów HTTP albo potrzebujesz zgodności wstecznej ze starszymi wersjami Node. Oba wybory są poprawne — decyzja zależy od tego, z jakich funkcji projekt naprawdę korzysta.
Dowiedz się więcej


