Fetch API stanowi fundamentalny element nowoczesnego web developmentu, umożliwiając programistom wykonywanie asynchronicznych żądań HTTP bezpośrednio z kodu JavaScript działającego w przeglądarce. Jako nowoczesna alternatywa dla starszego XMLHttpRequest, Fetch API oferuje czystszą składnię opartą na Promise’ach, lepszą obsługę błędów oraz wsparcie dla zaawansowanych funkcjonalności, takich jak streamowanie danych, obsługa nagłówków CORS i anulowanie żądań.
W niniejszym opracowaniu omówimy praktyczne wykorzystanie Fetch API: od pobierania i przetwarzania danych, przez obsługę błędów, po implementację wzorców niezbędnych do budowy współczesnych aplikacji webowych.
Fundamenty Fetch API i koncepcja komunikacji sieciowej
Fetch API reprezentuje zmianę paradygmatu w komunikacji JavaScript z serwerami. W przeciwieństwie do tradycyjnego XMLHttpRequest, który opiera się na systemie zdarzeń i callbackach, Fetch API wykorzystuje Promise’y, co pozwala na czytelniejsze pisanie kodu asynchronicznego.
Promise to obiekt JavaScript, który reprezentuje wartość dostępną teraz, w przyszłości lub nigdy, umożliwiając obsługę operacji asynchronicznych w sposób bardziej intuicyjny niż callbacki.
Fundamentalną funkcją Fetch API jest funkcja fetch(), która przyjmuje adres zasobu i opcjonalny obiekt konfiguracyjny. Zwracany Promise rozwiązuje się do obiektu Response (status, nagłówki, treść), co umożliwia łączenie operacji metodami .then() i .catch() lub użycie składni async/await.
Kluczowe jest zrozumienie, że Fetch API działa asynchronicznie i nie blokuje głównego wątku JavaScript, dzięki czemu interfejs pozostaje responsywny podczas oczekiwania na odpowiedź sieciową.
Wykonywanie podstawowych żądań GET i pobieranie danych
Żądanie GET służy do pobierania danych z serwera bez modyfikowania jego stanu. Poniższy przykład pokazuje podstawowe użycie fetch() i konwersji odpowiedzi do JSON:
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Błąd:', error));
W tym przykładzie obiekt Response zawiera strumień ReadableStream dostępny pod body. Metoda .json() konwertuje go do obiektu JavaScript i zwraca kolejny Promise. Metoda .catch() przechwytuje błędy z pobierania lub parsowania.
Nowocześniejszym i czytelniejszym podejściem jest użycie async/await wraz z obsługą wyjątków w try/catch:
async function fetchPost() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Błąd:', error);
}
}
fetchPost();
Przed przetworzeniem danych zawsze sprawdzaj status odpowiedzi. Właściwość response.ok (true dla 200–299) informuje o powodzeniu:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch error:', error.message);
}
}
Wysyłanie danych i żądań POST
Poza pobieraniem danych warto znać metody modyfikujące zasoby na serwerze. Poniżej szybkie przypomnienie, kiedy której używać:
- GET – pobieranie danych bez modyfikacji stanu serwera;
- POST – tworzenie nowych zasobów;
- PUT – pełne zastąpienie istniejącego zasobu;
- PATCH – częściowa aktualizacja zasobu (wybrane pola);
- DELETE – usuwanie zasobów.
Przykład tworzenia zasobu metodą POST z JSON-em w treści żądania:
async function createPost(postData) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Nowy post:', data);
} catch (error) {
console.error('Błąd:', error);
}
}
createPost({
title: 'Nowy post',
body: 'To jest treść nowego posta',
userId: 1,
});
Przykład częściowej aktualizacji metodą PATCH:
async function updatePost(postId, updateData) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updateData),
});
const data = await response.json();
console.log('Zaktualizowany post:', data);
} catch (error) {
console.error('Błąd:', error);
}
}
Usuwanie zasobów metodą DELETE:
async function deletePost(postId) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
method: 'DELETE',
});
if (response.ok) {
console.log('Post został usunięty');
}
} catch (error) {
console.error('Błąd:', error);
}
}
Obsługa różnych formatów odpowiedzi i konwersja danych
Serwer może zwracać różne formaty, a Fetch API oferuje dopasowane metody odczytu. Najczęstsze z nich to:
- .json() – konwersja do obiektu JavaScript z odpowiedzi JSON,
- .text() – zwykły tekst,
- .blob() – dane binarne (obrazy, pliki),
- .arrayBuffer() – niskopoziomowy bufor bajtów do dalszej obróbki.
Przykłady pobierania różnych formatów w praktyce:
async function fetchDifferentFormats() {
// Pobieranie JSON
const jsonResponse = await fetch('https://api.example.com/data.json');
const jsonData = await jsonResponse.json();
console.log('JSON data:', jsonData);
// Pobieranie tekstu
const textResponse = await fetch('https://example.com/file.txt');
const textData = await textResponse.text();
console.log('Text data:', textData);
// Pobieranie pliku binarnego (np. obrazu)
const imageResponse = await fetch('https://example.com/image.jpg');
const imageBlob = await imageResponse.blob();
const imageUrl = URL.createObjectURL(imageBlob);
document.getElementById('image').src = imageUrl;
}
Treść odpowiedzi można odczytać tylko raz. Jeśli potrzebujesz dostępu do danych w różnych formatach, użyj response.clone() i czytaj z dwóch kopii:
async function processResponseMultipleTimes() {
const response = await fetch('https://api.example.com/data');
const response2 = response.clone();
const jsonData = await response.json();
const textData = await response2.text();
console.log('JSON:', jsonData);
console.log('Text:', textData);
}
Obsługa XML wymaga użycia DOMParser do analizy tekstu XML:
async function fetchXML(url) {
try {
const response = await fetch(url);
const xmlText = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
console.log('Sparsowany XML:', xmlDoc);
} catch (error) {
console.error('Błąd:', error);
}
}
Obsługa błędów i strategie odporności
Fetch API nie odrzuca Promise’a dla statusów HTTP takich jak 404 czy 500 – odrzuca go tylko w przypadku błędu sieciowego (np. brak połączenia).
Typowe kategorie błędów, o których warto pamiętać:
- błąd sieciowy (TypeError) – brak łączności, przerwane połączenie lub błąd CORS,
- błąd HTTP (response.ok === false) – np. 400, 404, 500 zwracane przez serwer,
- błąd parsowania (SyntaxError) – niepoprawny JSON lub niezgodność formatu.
Poniżej przykład bezpiecznego pobierania z walidacją statusu i obsługą wyjątków:
async function robustFetch(url) {
try {
const response = await fetch(url);
// Sprawdzenie statusu HTTP
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error instanceof TypeError) {
console.error('Błąd sieciowy – nie można połączyć z serwerem');
} else if (error instanceof SyntaxError) {
console.error('Błąd parsowania JSON');
} else {
console.error('Nieznany błąd:', error.message);
}
throw error;
}
}
W celu poprawy niezawodności warto zaimplementować mechanizm automatycznych ponowień (retry) z rosnącym opóźnieniem:
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
console.warn(`Próba ${attempt} nie powiodła się. Ponowna próba...`);
// Czekaj przed następną próbą (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}
Kontrola czasu oczekiwania (timeout) zapobiega „wiszącym” żądaniom:
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Żądanie przekroczyło timeout');
}
throw error;
}
}
Zaawansowane funkcjonalności – AbortController i anulowanie żądań
AbortController umożliwia anulowanie jednego lub więcej żądań Fetch. To kluczowe w scenariuszach typu „search-as-you-type” czy zastępowanie starszego żądania nowszym:
let controller;
function startSearch(query) {
// Anuluj poprzednie żądanie
if (controller) {
controller.abort();
}
controller = new AbortController();
fetch(`https://api.example.com/search?q=${query}`, {
signal: controller.signal,
})
.then(response => response.json())
.then(data => console.log('Wyniki:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Wyszukiwanie zostało anulowane');
} else {
console.error('Błąd:', error);
}
});
}
function cancelSearch() {
if (controller) {
controller.abort();
}
}
Wygodny timeout zapewnia AbortSignal.timeout() – automatycznie anuluje żądanie po podanym czasie:
async function fetchWithBuiltInTimeout(url) {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(5000), // timeout po 5 sekundach
});
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Żądanie przekroczyło timeout');
} else {
console.error('Błąd:', error);
}
}
}
Można też połączyć wiele warunków anulowania dzięki AbortSignal.any():
async function fetchWithMultipleAbortConditions(url) {
const userAbortController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
try {
const response = await fetch(url, {
signal: AbortSignal.any([userAbortController.signal, timeoutSignal]),
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Żądanie zostało anulowane');
} else if (error.name === 'TimeoutError') {
console.error('Żądanie przekroczyło timeout');
} else {
console.error('Błąd:', error);
}
}
}
Nagłówki (Headers) i konfiguracja żądań
Nagłówki HTTP przenoszą metainformacje o żądaniu i odpowiedzi. Interfejs Headers umożliwia wygodne tworzenie i modyfikowanie nagłówków:
const myHeaders = new Headers();
myHeaders.set('Content-Type', 'application/json');
myHeaders.append('Authorization', 'Bearer token123');
myHeaders.append('X-Custom-Header', 'custom-value');
fetch('https://api.example.com/data', {
method: 'POST',
headers: myHeaders,
body: JSON.stringify({ name: 'John' }),
})
.then(response => response.json())
.then(data => console.log(data));
Alternatywnie można przekazać zwykły obiekt:
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
},
body: JSON.stringify({ name: 'John' }),
});
Dostęp do nagłówków odpowiedzi jest równie prosty:
async function inspectHeaders(url) {
const response = await fetch(url);
// Odczytaj konkretny nagłówek
const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType);
// Iteruj przez wszystkie nagłówki
response.headers.forEach((value, name) => {
console.log(`${name}: ${value}`);
});
}
Bezpieczeństwo, CORS i komunikacja między domenami
Cross-Origin Resource Sharing (CORS) kontroluje, czy przeglądarka pozwoli na żądania do innych domen. Domyślnie Fetch wykonuje żądania w trybie cors dla zasobów cross-origin.
Przykład żądań same-origin i cross-origin:
// Żądanie same-origin (ta sama domena) – zwykle bezproblemowe
fetch('/api/data')
.then(response => response.json());
// Żądanie cross-origin – wymaga poprawnych nagłówków CORS po stronie serwera
fetch('https://other-domain.com/api/data')
.then(response => response.json());
Na potrzeby CORS serwer musi odpowiedzieć właściwymi nagłówkami:
Access-Control-Allow-Origin: https://your-domain.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Oto najważniejsze wymagania CORS, o które musi zadbać serwer i/lub klient:
- Access-Control-Allow-Origin – musi wskazywać dozwodną domenę lub
*(bez cookies), - Access-Control-Allow-Methods – lista dozwolonych metod (np. GET, POST, PUT),
- Access-Control-Allow-Headers – lista nagłówków dozwolonych w żądaniu (np. Content-Type, Authorization),
- Access-Control-Allow-Credentials – wymagane, jeśli przesyłane są cookies/poświadczenia (wraz z
credentials: 'include').
Żądania „non-simple” (np. POST z JSON albo z niestandardowymi nagłówkami) wyzwalają preflight – automatyczne żądanie OPTIONS:
fetch('https://other-domain.com/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value',
},
body: JSON.stringify({ data: 'example' }),
});
// Przeglądarka automatycznie wyśle OPTIONS (preflight)
Jeśli żądanie wymaga uwierzytelnienia (cookies), ustaw credentials po stronie klienta oraz odpowiednie nagłówki po stronie serwera:
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include', // wyślij cookies wraz z żądaniem
})
.then(response => response.json())
.then(data => console.log(data));
Obsługa plików i uploadowanie danych
Uploadowanie plików zwykle wymaga użycia FormData zamiast JSON – przeglądarka sama ustawia poprawny nagłówek Content-Type z boundary.
Przykład wysyłania pliku:
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Mój plik');
try {
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData, // Nie ustawiaj ręcznie Content-Type
});
const result = await response.json();
console.log('Upload powodzenie:', result);
} catch (error) {
console.error('Błąd uploadu:', error);
}
}
// Użycie
document.getElementById('fileInput').addEventListener('change', (e) => {
const [file] = e.target.files;
if (file) uploadFile(file);
});
Pobieranie plików poprzez konwersję odpowiedzi do Blob:
async function downloadFile(url, filename) {
try {
const response = await fetch(url);
const blob = await response.blob();
// Utwórz link do pobrania
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error('Błąd pobierania:', error);
}
}
Caching i optymalizacja wydajności
Caching to kluczowa technika optymalizacji wydajności aplikacji, którą można realizować zarówno nagłówkami HTTP, jak i Service Workerem.
Przykład ustawienia polityki cache’u w żądaniu (faktyczny caching kontroluje odpowiedź serwera i/lub cache przeglądarki):
// Cache na 1 godzinę (zależnie od wsparcia po stronie serwera/proxy)
fetch('https://api.example.com/data', {
headers: {
'Cache-Control': 'max-age=3600',
},
})
.then(response => response.json());
Service Worker pozwala wdrażać strategie „offline-first” i kontrolę nad cachem aplikacyjnym:
const CACHE_NAME = 'v1';
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Zwróć z cache, jeśli dostępne
if (response) {
return response;
}
// W przeciwnym razie pobierz z sieci
return fetch(event.request).then((networkResponse) => {
// Zapisz w cache
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return networkResponse;
});
})
.catch(() => {
// Fallback, jeśli offline
return caches.match('/offline.html');
})
);
});
Warto znać popularne strategie przyspieszające odpowiedzi i zmniejszające obciążenie sieci:
- stale-while-revalidate – natychmiastowa odpowiedź z cache’u z równoległą rewalidacją danych,
- ETag/If-None-Match – walidacja warunkowa i oszczędność transferu,
- prefetch/prerender – wcześniejsze pobieranie kluczowych zasobów,
- agresywny cache statycznych zasobów – długie TTL z fingerprintingiem plików.
Przykład nagłówka z dyrektywą stale-while-revalidate:
fetch('https://api.example.com/data', {
headers: {
'Cache-Control': 'max-age=60, stale-while-revalidate=3600',
},
})
.then(response => response.json());
Porównanie Fetch API z XMLHttpRequest
Poniższa tabela syntetycznie porównuje wybrane aspekty obu rozwiązań:
| Aspekt | Fetch API | XMLHttpRequest |
|---|---|---|
| Obsługa błędów | Tylko błędy sieciowe odrzucają Promise | Brak Promise, event-based |
| Parsing JSON | Manualny (.json()) |
Manualny (JSON.parse()) |
| Timeouty | Wymaga AbortController | Wbudowana właściwość timeout |
| Tracking postępu | Trudne z response body | Wbudowany event progress |
| Anulowanie | AbortController | Metoda .abort() |
| Wsparcie przeglądarek | Nowoczesne przeglądarki | Wszystkie przeglądarki |
W nowych projektach zazwyczaj preferuje się Fetch API ze względu na czystsze API i natywną obsługę Promise’ów.
Praktyczne zastosowania i przykłady real-world
Fetch API sprawdza się w wielu scenariuszach: od pobierania list produktów, przez integracje z zewnętrznymi API, po synchronizację danych. Poniżej mini-aplikacja pobierająca dane z PokéAPI:
async function searchPokemon() {
const pokemonName = document.getElementById('searchInput').value.toLowerCase();
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`);
if (!response.ok) {
throw new Error('Pokémon nie znaleziony');
}
const data = await response.json();
displayPokemon(data);
} catch (error) {
console.error('Błąd:', error);
displayError(error.message);
}
}
function displayPokemon(data) {
document.getElementById('pokemonName').textContent = data.name;
document.getElementById('pokemonImage').src = data.sprites.front_default;
document.getElementById('pokemonType').textContent = data.types.map(t => t.type.name).join(', ');
}
Zaawansowane strategie – streamowanie i obsługa dużych danych
Streamowanie pozwala przetwarzać duże dane fragmentami zamiast ładować całość do pamięci:
async function processLargeFile(url) {
const response = await fetch(url);
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Przetwórz fragment danych
const chunk = new TextDecoder().decode(value);
console.log('Otrzymany fragment:', chunk);
}
} catch (error) {
console.error('Błąd streamingu:', error);
}
}
Nowsze API ReadableStream z pętlą for await…of upraszcza obsługę strumieni:
async function streamLines(url) {
const response = await fetch(url);
for await (const chunk of response.body) {
const text = new TextDecoder().decode(chunk);
// Przetwórz każdy fragment
processChunk(text);
}
}
Debugowanie i troubleshooting
Narzędzia deweloperskie przeglądarki (DevTools, zakładka Network) pomagają diagnozować problemy z żądaniami. Dodatkowo możesz logować szczegóły każdego wywołania:
// Loguj szczegółowe informacje o żądaniu
async function debugFetch(url) {
console.log('Wysyłanie żądania do:', url);
const startTime = performance.now();
try {
const response = await fetch(url);
const endTime = performance.now();
console.log('Status:', response.status);
console.log('Status Text:', response.statusText);
console.log('Czas odpowiedzi:', `${(endTime - startTime).toFixed(2)}ms`);
console.log('Nagłówki:', Object.fromEntries(response.headers));
const data = await response.json();
console.log('Dane:', data);
return data;
} catch (error) {
console.error('Błąd Fetch:', error);
console.error('Typ błędu:', error.name);
console.error('Wiadomość:', error.message);
}
}