Fetch API – jak pobierać dane z serwera w JavaScript?

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);
}
}

Programista i twórca serwisu Creative Coding, absolwent Politechniki Warszawskiej (WEiTI). Od 10+ lat łączy front‑end, grafikę generatywną i narzędzia dla twórców; opublikował 120+ projektów i artykułów, prowadził warsztaty dla 2 000+ uczestników. Pracuje z JavaScriptem, Three.js, P5.js i GLSL, bada wydajność i dokumentuje procesy, tworząc praktyczne przewodniki dla osób łączących kod z obrazem, dźwiękiem i interakcją.
Zostaw komentarz

Komentarze

Brak komentarzy. Dlaczego nie rozpoczniesz dyskusji?

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *