Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

Niemal każda nowoczesna aplikacja webowa komunikuje się z serwerem – pobiera dane, wysyła formularze, aktualizuje zasoby. Frontend (nasza aplikacja React) musi umieć rozmawiać z backendem (serwerem). Do tego służy API (Application Programming Interface – interfejs programistyczny aplikacji), czyli zestaw endpoints (punktów końcowych, adresów URL) przez które frontend i backend wymieniają dane.

W TypeScript mamy dwa główne narzędzia do komunikacji z API: wbudowany Fetch API oraz popularną bibliotekę Axios. Fetch jest prostszy i dostępny natywnie w przeglądarkach, ale Axios oferuje więcej funkcjonalności i jest wygodniejszy w użyciu.

🔍 Fetch vs Axios - szybkie porównanie:
Fetch Axios
Instalacja ✅ Wbudowany (0 KB) ❌ npm install (~13 KB)
Automatyczne JSON ❌ Ręcznie .json() ✅ Automatycznie
Obsługa błędów ❌ Ręcznie sprawdzać ✅ Automatycznie
Interceptors ❌ Nie ma ✅ Tak
Timeout ❌ Ręczny ✅ Wbudowany

Rekomendacja: Dla prostych projektów - Fetch. Dla większych aplikacji - Axios! 🎯

W tym wpisie nauczymy się profesjonalnie komunikować z API. Poznamy zarówno Fetch jak i Axios, nauczymy się obsługiwać błędy, dodawać interceptory (przechwytywacze żądań, które pozwalają zmodyfikować request przed wysłaniem lub response po otrzymaniu), uploadować pliki, typować odpowiedzi TypeScriptem. To będzie bardzo praktyczny wpis – po nim będziesz mógł zintegrować frontend z dowolnym backendem!

Fetch API – podstawy

Fetch to wbudowane API przeglądarek do wykonywania żądań HTTP (protokół komunikacji w internecie).

🔍 Co to jest HTTP?

HTTP (HyperText Transfer Protocol) = "język" którym przeglądarka rozmawia z serwerem.

Metody HTTP:

  • GET - pobierz dane (czytanie)"
  • POST - stwórz nowy zasób (tworzenie)
  • PUT - zastąp cały zasób (pełna aktualizacja)
  • PATCH - zmień część zasobu (częściowa aktualizacja)
  • DELETE - usuń zasób (usuwanie)

Prosty GET request

GET to metoda HTTP służąca do pobierania danych z serwera (czytanie, bez modyfikacji).

// Podstawowy fetch
async function fetchUsers() {
  const response = await fetch('https://api.example.com/users');
  const users = await response.json();
  console.log(users);
}
🔍 Co tu się dzieje krok po kroku:
  1. fetch('...') - wysyła request GET do serwera
  2. await - czeka aż serwer odpowie (może trwać sekundy!)
  3. response - obiekt z odpowiedzią serwera (status, headers, body)
  4. response.json() - parsuje body jako JSON (też async!)
  5. users - gotowe dane! Tablica użytkowników

Dlaczego dwa await? Bo fetch działa w dwóch krokach:

  • Pierwszy await - czeka na headers (status, metadata)
  • Drugi await (.json()) - czeka na body (dane)

Z obsługą błędów

⚠️ WAŻNE: Fetch NIE rzuca błędu dla 404 czy 500!

To największy "gotcha" w Fetch. Dla Fetch, każda odpowiedź od serwera = sukces, nawet 404 Not Found czy 500 Server Error!

Musisz ręcznie sprawdzać response.ok:

const response = await fetch('/api/users');
if (!response.ok) {  // ← Bez tego nie złapiesz błędów!
  throw new Error(`HTTP ${response.status}`);
}
async function fetchUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    
    // Fetch NIE rzuca błędu dla 404, 500, itp!
    // Musimy ręcznie sprawdzić status
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const users = await response.json();
    return users;
  } catch (error) {
    console.error('Error fetching users:', error);
    throw error;
  }
}
🌟 Przykład: Co się stanie BEZ sprawdzenia response.ok
// ❌ ZŁY KOD - nie sprawdza błędów
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Scenariusz:
// 1. Użytkownik o ID 999 nie istnieje
// 2. Serwer zwraca 404 Not Found
// 3. Fetch MYŚLI że to sukces! 
// 4. response.json() próbuje sparsować error page jako JSON
// 5. 💥 SyntaxError: Unexpected token < in JSON
// 6. Użytkownik widzi zagadkowy błąd zamiast "Użytkownik nie znaleziony"

// ✅ DOBRY KOD - sprawdza błędy
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  
  if (!response.ok) {
    if (response.status === 404) {
      throw new Error('Użytkownik nie znaleziony');
    }
    throw new Error(`HTTP ${response.status}`);
  }
  
  const user = await response.json();
  return user;
}

POST request (wysyłanie danych)

POST to metoda HTTP służąca do tworzenia nowych zasobów na serwerze (np. nowy użytkownik, nowy post).

🔍 Kiedy używać POST?
  • ✅ Tworzenie nowego użytkownika (rejestracja)
  • ✅ Dodawanie nowego posta/komentarza
  • ✅ Wysyłanie formularza kontaktowego
  • ✅ Upload pliku
  • ✅ Logowanie (wysyłasz email/hasło)

Ogólna reguła: POST = "stwórz coś nowego" 📝

interface User {
  name: string;
  email: string;
}

async function createUser(user: User) {
  const response = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json', // Informujemy że wysyłamy JSON
    },
    body: JSON.stringify(user), // Konwertujemy obiekt na JSON string
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

// Użycie
createUser({ name: 'Jan', email: 'jan@example.com' });
🔍 Dlaczego JSON.stringify()?

HTTP może wysyłać TYLKO tekst. Nie możesz wysłać obiektu JavaScript bezpośrednio!

// ❌ To NIE ZADZIAŁA
body: { name: 'Jan', email: 'jan@test.com' }  // Obiekt!

// ✅ To DZIAŁA
body: JSON.stringify({ name: 'Jan', email: 'jan@test.com' })  // String!
// Wynik: '{"name":"Jan","email":"jan@test.com"}'

JSON.stringify() konwertuje obiekt JavaScript → JSON string
Content-Type: application/json mówi serwerowi: "hej, wysyłam JSON!"

🌟 Przykład: POST w praktyce - formularz rejestracji
function RegistrationForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: ''
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Rejestracja nie powiodła się');
      }

      const user = await response.json();
      console.log('Zarejestrowano:', user);
      navigate('/login'); // Redirect na login

    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      
      <input 
        value={formData.name}
        onChange={(e) => setFormData({...formData, name: e.target.value})}
        placeholder="Imię"
      />
      
      <input 
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({...formData, email: e.target.value})}
        placeholder="Email"
      />
      
      <input 
        type="password"
        value={formData.password}
        onChange={(e) => setFormData({...formData, password: e.target.value})}
        placeholder="Hasło"
      />
      
      <button type="submit" disabled={loading}>
        {loading ? 'Rejestrowanie...' : 'Zarejestruj się'}
      </button>
    </form>
  );
}

PUT/PATCH request (aktualizacja)

PUT i PATCH to metody HTTP służące do aktualizacji istniejących zasobów. PUT zastępuje cały zasób, PATCH modyfikuje tylko wybrane pola.

🔍 PUT vs PATCH - jaka różnica?

Wyobraź sobie że edytujesz profil użytkownika:

PUT - zastępuje WSZYSTKO:

// Użytkownik przed:
{ name: 'Jan', email: 'jan@test.com', age: 25, city: 'Warszawa' }

// PUT /users/1
{ name: 'Jan', email: 'nowy@test.com' }

// Użytkownik po:
{ name: 'Jan', email: 'nowy@test.com' }  // age i city ZNIKŁY!

PATCH - zmienia TYLKO wskazane pola:

// Użytkownik przed:
{ name: 'Jan', email: 'jan@test.com', age: 25, city: 'Warszawa' }

// PATCH /users/1
{ email: 'nowy@test.com' }

// Użytkownik po:
{ name: 'Jan', email: 'nowy@test.com', age: 25, city: 'Warszawa' }  // Reszta zostaje!

Reguła: PUT = "zastąp całość", PATCH = "zmień kawałek" 🔧

// PUT - zastąp cały zasób
async function updateUser(id: number, user: User) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(user),
  });

  return response.json();
}

// PATCH - zaktualizuj tylko wybrane pola
async function updateUserEmail(id: number, email: string) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email }), // Tylko email
  });

  return response.json();
}

DELETE request (usuwanie)

DELETE to metoda HTTP służąca do usuwania zasobów z serwera.

🔍 DELETE - usuwanie zasobów

DELETE = "usuń to z serwera" 🗑️

Status 204 No Content: Wiele API zwraca 204 gdy usuwa zasób. To znaczy:

  • ✅ Sukces! Zasób usunięty
  • ✅ Brak treści w odpowiedzi (nie ma czego zwracać - zasób już nie istnieje!)
async function deleteUser(id: number) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'DELETE',
  });

  if (!response.ok) {
    throw new Error('Failed to delete user');
  }

  // DELETE często zwraca status 204 (No Content) - brak treści w odpowiedzi
  if (response.status === 204) {
    return null;
  }

  return response.json();
}

Timeout (limit czasu)

Fetch nie ma wbudowanego timeoutu. Musimy go sami zaimplementować używając AbortController (mechanizm do anulowania żądań).

🔍 Po co timeout?

Bez timeoutu: Fetch czeka W NIESKOŃCZONOŚĆ. Jeśli serwer nie odpowie, użytkownik czeka... i czeka... i czeka... ♾️

Z timeoutem: Po 5 sekundach (lub innym czasie) - anuluj żądanie i pokaż błąd. Użytkownik wie że coś nie tak! ⏱️

AbortController = narzędzie do anulowania żądań. Działa jak "przycisk STOP" dla fetch.

async function fetchWithTimeout(url: string, timeout: number = 5000) {
  const controller = new AbortController(); // Kontroler do anulowania żądania
  const timeoutId = setTimeout(() => controller.abort(), timeout); // Anuluj po timeoucie

  try {
    const response = await fetch(url, {
      signal: controller.signal, // Przekaż signal do fetch
    });
    clearTimeout(timeoutId); // Wyczyść timeout jeśli sukces
    return response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw error;
  }
}
🌟 Przykład: Timeout w praktyce
function useApiWithTimeout() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);

    async function fetchData() {
      try {
        const response = await fetch('/api/slow-endpoint', {
          signal: controller.signal
        });
        
        clearTimeout(timeoutId);
        
        if (!response.ok) throw new Error('HTTP error');
        
        const json = await response.json();
        setData(json);
        
      } catch (err) {
        if (err.name === 'AbortError') {
          setError('Timeout - serwer zbyt długo odpowiada');
        } else {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

    return () => {
      clearTimeout(timeoutId);
      controller.abort(); // Anuluj przy unmount
    };
  }, []);

  return { data, loading, error };
}
Axios – lepsza alternatywa

Axios to popularna biblioteka do żądań HTTP, która oferuje wygodniejsze API i więcej funkcjonalności niż Fetch.

Instalacja

npm install axios

Podstawowe użycie

🔍 Dlaczego Axios jest prostszy?

Porównaj ten sam kod w Fetch vs Axios:

Fetch (7 linii, dużo ręcznej roboty):

const response = await fetch('/api/users');
if (!response.ok) {  // ← ręczne sprawdzanie
  throw new Error('HTTP error');
}
const data = await response.json();  // ← ręczne parsowanie
console.log(data);

Axios (2 linie, wszystko automatyczne!):

const response = await axios.get('/api/users');
console.log(response.data);  // Gotowe! ✨
import axios from 'axios';

// GET
const users = await axios.get('https://api.example.com/users');
console.log(users.data); // Dane są w .data

// POST
const newUser = await axios.post('https://api.example.com/users', {
  name: 'Jan',
  email: 'jan@example.com'
});

// PUT
await axios.put('https://api.example.com/users/1', userData);

// PATCH
await axios.patch('https://api.example.com/users/1', { email: 'new@example.com' });

// DELETE
await axios.delete('https://api.example.com/users/1');

Zalety Axios vs Fetch

  • Automatyczna konwersja JSON – nie musisz ręcznie wywoływać .json()
  • Rzuca błędy dla 4xx/5xx – automatyczna obsługa błędnych statusów
  • Timeout wbudowany – prostsze zarządzanie czasem oczekiwania
  • Interceptory – przechwytuj i modyfikuj wszystkie żądania/odpowiedzi
  • Progress events – śledzenie postępu uploadu/downloadu
  • Automatyczne XSRF protection – ochrona przed atakami cross-site
  • Cancel tokens – łatwiejsze anulowanie żądań
🌟 Przykład: Ta sama funkcja w Fetch vs Axios
// ❌ Z FETCH - długie, dużo ręcznej roboty
async function getUserFetch(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    
    if (!response.ok) {
      if (response.status === 404) {
        throw new Error('Użytkownik nie znaleziony');
      }
      throw new Error(`HTTP ${response.status}`);
    }
    
    const user = await response.json();
    return user;
    
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

// ✅ Z AXIOS - krótkie, czytelne
async function getUserAxios(id) {
  try {
    const response = await axios.get(`/api/users/${id}`);
    return response.data;
  } catch (error) {
    if (error.response?.status === 404) {
      throw new Error('Użytkownik nie znaleziony');
    }
    throw error;
  }
}

Axios: mniej kodu = mniej błędów = szczęśliwszy developer! 😊

Konfiguracja Axios instance

Instance (instancja) to skonfigurowany obiekt Axios z domyślnymi ustawieniami.

🔍 Po co tworzyć instance?

Bez instance - powtarzasz URL w każdym żądaniu:

axios.get('https://api.myapp.com/users');
axios.get('https://api.myapp.com/posts');
axios.get('https://api.myapp.com/comments');

Z instance - ustawiasz raz, używasz wszędzie:

const api = axios.create({
  baseURL: 'https://api.myapp.com'
});

api.get('/users');     // → https://api.myapp.com/users
api.get('/posts');     // → https://api.myapp.com/posts
api.get('/comments');  // → https://api.myapp.com/comments

DRY (Don't Repeat Yourself) w najlepszym wydaniu! 🎯

// api/client.ts
import axios from 'axios';

// Utwórz instancję z domyślną konfiguracją
export const apiClient = axios.create({
  baseURL: 'https://api.example.com', // Bazowy URL - będzie dodawany do wszystkich żądań
  timeout: 10000, // 10 sekund timeout
  headers: {
    'Content-Type': 'application/json',
  },
});

// Teraz możesz użyć krótkiej ścieżki
apiClient.get('/users'); // Zamiast pełnego URL
apiClient.post('/users', userData);

Środowiskowe URL (development vs production)

🔍 Zmienne środowiskowe - różne API dla dev/prod

Podczas developmentu: używasz lokalnego backendu (localhost:3000)
Na produkcji: używasz prawdziwego backendu (api.myapp.com)

Zmienne środowiskowe pozwalają automatycznie wybrać właściwy URL! 🔄

// api/client.ts
const baseURL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';

export const apiClient = axios.create({
  baseURL,
  timeout: 10000,
});
# .env.development
VITE_API_URL=http://localhost:3000/api

# .env.production
VITE_API_URL=https://api.myapp.com
🌟 Przykład: Kompletna konfiguracja API client
// api/client.ts
import axios from 'axios';

const isDevelopment = import.meta.env.DEV;

export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
  timeout: isDevelopment ? 30000 : 10000, // Dłuższy timeout w dev (debugowanie)
  headers: {
    'Content-Type': 'application/json',
  },
});

// Log tylko w development
if (isDevelopment) {
  apiClient.interceptors.request.use((config) => {
    console.log('📤 API Request:', config.method?.toUpperCase(), config.url);
    return config;
  });
  
  apiClient.interceptors.response.use(
    (response) => {
      console.log('📥 API Response:', response.status, response.config.url);
      return response;
    },
    (error) => {
      console.error('❌ API Error:', error.message);
      return Promise.reject(error);
    }
  );
}

export default apiClient;
TypeScript w API calls

Typowanie odpowiedzi

🔍 Dlaczego typować API responses?

Bez typów - nie wiesz co dostaniesz z API:

const response = await axios.get('/users/1');
console.log(response.data.name);  // Może być undefined! 💥
console.log(response.data.emial); // Literówka! Nie wyłapiesz! 💥

Z typami - TypeScript Cię chroni:

const response = await axios.get<User>('/users/1');
console.log(response.data.name);  // ✅ TypeScript wie że to string
console.log(response.data.emial); // ❌ ERROR! "emial" nie istnieje na User

Łapiesz błędy PRZED uruchomieniem! 🛡️

// types/api.ts
export interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
}

export interface ApiResponse<T> {
  data: T;
  message?: string;
  status: number;
}
// api/users.ts
import { apiClient } from './client';
import { User, ApiResponse } from '../types/api';

// GET
export async function getUsers(): Promise<User[]> {
  const response = await apiClient.get<ApiResponse<User[]>>('/users');
  return response.data.data;
}

// GET by ID
export async function getUser(id: number): Promise<User> {
  const response = await apiClient.get<ApiResponse<User>>(`/users/${id}`);
  return response.data.data;
}

// POST
export async function createUser(userData: Omit<User, 'id'>): Promise<User> {
  const response = await apiClient.post<ApiResponse<User>>('/users', userData);
  return response.data.data;
}

// PUT
export async function updateUser(id: number, userData: Partial<User>): Promise<User> {
  const response = await apiClient.put<ApiResponse<User>>(`/users/${id}`, userData);
  return response.data.data;
}

// DELETE
export async function deleteUser(id: number): Promise<void> {
  await apiClient.delete(`/users/${id}`);
}
🔍 Omit vs Partial - jaka różnica?

Omit<User, 'id'> - wszystkie pola User OPRÓCZ id:

// User = { id, name, email, avatar? }
// Omit = { name, email, avatar? }

createUser({
  name: 'Jan',      // ✅ Wymagane
  email: 'jan@...'  // ✅ Wymagane
  // id - NIE podajesz (serwer go wygeneruje)
});

Partial<User> - wszystkie pola User OPCJONALNE:

// User = { id, name, email, avatar? }
// Partial = { id?, name?, email?, avatar? }

updateUser(1, {
  email: 'nowy@email.com'  // ✅ Tylko email, reszta zostaje
});
💪 Ćwiczenie 1: Typowany API client dla Posts

Stwórz kompletny API client dla postów bloga:

  1. Zdefiniuj interface Post (id, title, content, authorId, createdAt)
  2. Napisz 5 funkcji:
    • getPosts() - GET /posts
    • getPost(id) - GET /posts/:id
    • createPost(data) - POST /posts
    • updatePost(id, data) - PATCH /posts/:id
    • deletePost(id) - DELETE /posts/:id
  3. Wszystkie funkcje muszą być typowane (Promise<Post>, Omit, Partial)
Interceptory (przechwytywacze)

Interceptory to funkcje, które przechwytują każde żądanie (request interceptor) przed wysłaniem lub każdą odpowiedź (response interceptor) po otrzymaniu. Pozwalają globalnie modyfikować requesty/responses.

🔍 Co to są interceptory?

Interceptor = "strażnik" który zatrzymuje każde żądanie/odpowiedź i może coś z nim zrobić.

Request interceptor: Działa PRZED wysłaniem żądania

  • Dodaj token autoryzacji do KAŻDEGO żądania
  • Dodaj custom headers
  • Zaloguj żądanie do konsoli

Response interceptor: Działa PO otrzymaniu odpowiedzi

  • Globalnie obsłuż błędy (401 → wyloguj, 500 → toast)
  • Przekształć dane
  • Zaloguj odpowiedź do konsoli

Piszesz raz, działa dla WSZYSTKICH żądań! 🎯

Request interceptor (dodawanie tokenu autoryzacji)

// api/client.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: 'https://api.example.com',
});

// Dodaj interceptor dla żądań (requests)
apiClient.interceptors.request.use(
  (config) => {
    // Pobierz token z localStorage
    const token = localStorage.getItem('auth_token');
    
    if (token) {
      // Dodaj token do nagłówka Authorization
      config.headers.Authorization = `Bearer ${token}`;
    }
    
    return config; // Zwróć zmodyfikowany config
  },
  (error) => {
    // Obsłuż błąd przed wysłaniem żądania
    return Promise.reject(error);
  }
);

Teraz każde żądanie automatycznie ma token w nagłówku!

🔍 Jak działa request interceptor krok po kroku:
  1. Wywołujesz: apiClient.get('/users')
  2. Axios zatrzymuje żądanie: "Czekaj! Przejdź przez interceptor"
  3. Interceptor: Pobiera token z localStorage
  4. Interceptor: Dodaje Authorization: Bearer abc123 do headers
  5. Interceptor: Zwraca zmodyfikowany config
  6. Axios: "OK, teraz mogę wysłać" → wysyła żądanie

Efekt: NIE MUSISZ dodawać tokenu ręcznie w każdym żądaniu! 🎉

Response interceptor (globalna obsługa błędów)

import { toast } from 'react-hot-toast'; // Biblioteka do powiadomień

apiClient.interceptors.response.use(
  (response) => {
    // Zwróć odpowiedź jeśli sukces (status 2xx)
    return response;
  },
  (error) => {
    // Obsłuż różne błędy globalnie
    
    if (error.response) {
      // Serwer odpowiedział z błędem
      const status = error.response.status;
      
      switch (status) {
        case 401: // Unauthorized - brak autoryzacji
          toast.error('Sesja wygasła. Zaloguj się ponownie.');
          localStorage.removeItem('auth_token');
          window.location.href = '/login';
          break;
          
        case 403: // Forbidden - brak uprawnień
          toast.error('Nie masz uprawnień do tej operacji.');
          break;
          
        case 404: // Not Found - nie znaleziono
          toast.error('Zasób nie znaleziony.');
          break;
          
        case 500: // Internal Server Error - błąd serwera
          toast.error('Błąd serwera. Spróbuj ponownie później.');
          break;
          
        default:
          toast.error('Wystąpił błąd. Spróbuj ponownie.');
      }
    } else if (error.request) {
      // Żądanie zostało wysłane ale brak odpowiedzi (np. brak internetu)
      toast.error('Brak połączenia z serwerem.');
    } else {
      // Coś poszło nie tak przy tworzeniu żądania
      toast.error('Wystąpił nieoczekiwany błąd.');
    }
    
    return Promise.reject(error);
  }
);
🔍 error.response vs error.request vs error.message

error.response - serwer odpowiedział z błędem (400, 404, 500):

// Serwer zwrócił 404
error.response.status === 404
error.response.data === { message: 'Not found' }

error.request - żądanie wysłane, brak odpowiedzi (timeout, brak internetu):

// Brak internetu lub timeout
error.request !== undefined
// ale error.response === undefined

error.message - błąd przy tworzeniu żądania (np. niepoprawny URL):

// Literówka w URL
error.message === 'Invalid URL'

Logging interceptor (logowanie do konsoli)

// Interceptor do debugowania w development
if (import.meta.env.DEV) {
  apiClient.interceptors.request.use((config) => {
    console.log('📤 Request:', config.method?.toUpperCase(), config.url);
    return config;
  });

  apiClient.interceptors.response.use(
    (response) => {
      console.log('📥 Response:', response.status, response.config.url);
      return response;
    },
    (error) => {
      console.error('❌ Error:', error.message);
      return Promise.reject(error);
    }
  );
}
Upload plików

Single file upload (jeden plik)

🔍 Co to jest FormData?

FormData = specjalny format do wysyłania plików przez HTTP. Zwykły JSON nie może zawierać plików!

// ❌ To NIE ZADZIAŁA - JSON nie obsługuje plików
const data = {
  file: myFile,  // File object
  description: 'My photo'
};
axios.post('/upload', data);  // BŁĄD!

// ✅ To DZIAŁA - FormData obsługuje pliki
const formData = new FormData();
formData.append('file', myFile);
formData.append('description', 'My photo');
axios.post('/upload', formData);  // ✅ Sukces!

Content-Type: multipart/form-data mówi serwerowi: "wysyłam pliki!" 📎

async function uploadFile(file: File): Promise<string> {
  const formData = new FormData(); // Specjalny format do wysyłania plików
  formData.append('file', file); // Dodaj plik
  formData.append('description', 'My file'); // Możesz dodać dodatkowe dane

  const response = await apiClient.post<{ url: string }>('/upload', formData, {
    headers: {
      'Content-Type': 'multipart/form-data', // Informujemy że wysyłamy pliki
    },
    onUploadProgress: (progressEvent) => {
      // Śledź postęp uploadu
      const percentCompleted = Math.round(
        (progressEvent.loaded * 100) / (progressEvent.total || 1)
      );
      console.log(`Upload: ${percentCompleted}%`);
    },
  });

  return response.data.url; // URL załadowanego pliku
}
🔍 onUploadProgress - śledzenie postępu

onUploadProgress wywołuje się wielokrotnie podczas uploadu:

onUploadProgress: (progressEvent) => {
  // progressEvent.loaded = ile bajtów już wysłano
  // progressEvent.total = całkowity rozmiar pliku
  
  const percent = (progressEvent.loaded / progressEvent.total) * 100;
  console.log(`${percent}%`);
}

// Przykład dla pliku 10MB:
// 0% → 10% → 25% → 50% → 75% → 100% ✅

Dzięki temu możesz pokazać progress bar użytkownikowi! 📊

Multiple files upload (wiele plików)

async function uploadMultipleFiles(files: File[]): Promise<string[]> {
  const formData = new FormData();
  
  // Dodaj wszystkie pliki
  files.forEach((file) => {
    formData.append('files', file); // Ta sama nazwa dla wszystkich
  });

  const response = await apiClient.post<{ urls: string[] }>(
    '/upload-multiple',
    formData,
    {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    }
  );

  return response.data.urls;
}

Upload z React

import { useState } from 'react';

function FileUpload() {
  const [file, setFile] = useState<File | null>(null);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  async function handleUpload() {
    if (!file) return;

    setUploading(true);
    setProgress(0);

    try {
      const formData = new FormData();
      formData.append('file', file);

      await apiClient.post('/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / (progressEvent.total || 1)
          );
          setProgress(percentCompleted);
        },
      });

      toast.success('Plik załadowany!');
    } catch (error) {
      toast.error('Błąd podczas uploadu');
    } finally {
      setUploading(false);
    }
  }

  return (
    <div>
      <input
        type="file"
        onChange={(e) => setFile(e.target.files?.[0] || null)}
        disabled={uploading}
      />
      
      <button onClick={handleUpload} disabled={!file || uploading}>
        {uploading ? `Uploading... ${progress}%` : 'Upload'}
      </button>

      {uploading && (
        <div className="progress-bar">
          <div style={{ width: `${progress}%` }} />
        </div>
      )}
    </div>
  );
}
🌟 Przykład: Upload z preview i drag&drop
function ImageUpload() {
  const [file, setFile] = useState<File | null>(null);
  const [preview, setPreview] = useState<string | null>(null);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const handleFileSelect = (selectedFile: File) => {
    setFile(selectedFile);
    
    // Stwórz preview
    const reader = new FileReader();
    reader.onloadend = () => {
      setPreview(reader.result as string);
    };
    reader.readAsDataURL(selectedFile);
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    const droppedFile = e.dataTransfer.files[0];
    if (droppedFile && droppedFile.type.startsWith('image/')) {
      handleFileSelect(droppedFile);
    }
  };

  const handleUpload = async () => {
    if (!file) return;

    const formData = new FormData();
    formData.append('image', file);

    setUploading(true);

    try {
      const response = await apiClient.post('/upload/image', formData, {
        onUploadProgress: (e) => {
          setProgress(Math.round((e.loaded * 100) / (e.total || 1)));
        },
      });

      toast.success('Zdjęcie załadowane!');
      console.log('URL:', response.data.url);
      
    } catch (error) {
      toast.error('Błąd uploadu');
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <div
        onDrop={handleDrop}
        onDragOver={(e) => e.preventDefault()}
        style={{
          border: '2px dashed #ccc',
          padding: '40px',
          textAlign: 'center',
          cursor: 'pointer'
        }}
      >
        {preview ? (
          <img src={preview} alt="Preview" style={{ maxWidth: '200px' }} />
        ) : (
          <p>Przeciągnij zdjęcie lub kliknij aby wybrać</p>
        )}
        
        <input
          type="file"
          accept="image/*"
          onChange={(e) => {
            const file = e.target.files?.[0];
            if (file) handleFileSelect(file);
          }}
          style={{ display: 'none' }}
          id="file-input"
        />
        <label htmlFor="file-input">Wybierz plik</label>
      </div>

      {file && (
        <div>
          <p>{file.name} ({(file.size / 1024).toFixed(2)} KB)</p>
          <button onClick={handleUpload} disabled={uploading}>
            {uploading ? `${progress}%` : 'Upload'}
          </button>
        </div>
      )}
    </div>
  );
}
💪 Ćwiczenie 2: Upload wielu plików z podglądam

Stwórz komponent do uploadu wielu zdjęć:

  1. Pozwala wybrać wiele plików naraz (multiple)
  2. Pokazuje preview każdego zdjęcia
  3. Dla każdego pliku osobny progress bar
  4. Możliwość usunięcia pliku przed uploadem
  5. Upload wszystkich plików jednocześnie (Promise.all)
  6. Pokazuje sumaryczny progress (średnia ze wszystkich)
Cancel requests (anulowanie żądań)

Przydatne gdy użytkownik zmienia stronę lub wpisuje w search bar.

🔍 Po co anulować żądania?

Scenariusz bez cancel:

  1. Użytkownik wpisuje "Jan" → wysyłasz request
  2. Użytkownik wpisuje "Janek" → wysyłasz request
  3. Użytkownik wpisuje "Janusz" → wysyłasz request
  4. 3 requesty lecą do serwera! 💸
  5. Odpowiedzi przychodzą w losowej kolejności
  6. Pokazujesz wyniki dla "Jan" zamiast "Janusz" 😱

Scenariusz z cancel:

  1. Użytkownik wpisuje "Jan" → wysyłasz request
  2. Użytkownik wpisuje "Janek" → ANULUJESZ pierwszy, wysyłasz nowy
  3. Użytkownik wpisuje "Janusz" → ANULUJESZ drugi, wysyłasz nowy
  4. Tylko 1 request dociera do końca! ✅

Axios cancel token

import axios, { CancelTokenSource } from 'axios';

let cancelTokenSource: CancelTokenSource | null = null;

async function searchUsers(query: string) {
  // Anuluj poprzednie żądanie jeśli istnieje
  if (cancelTokenSource) {
    cancelTokenSource.cancel('New search initiated');
  }

  // Utwórz nowy cancel token
  cancelTokenSource = axios.CancelToken.source();

  try {
    const response = await apiClient.get('/users/search', {
      params: { q: query },
      cancelToken: cancelTokenSource.token,
    });
    
    return response.data;
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('Request cancelled:', error.message);
    } else {
      throw error;
    }
  }
}

Z React

🔍 Debounce + Cancel - idealne combo!

Debounce = czeka 500ms od ostatniego wpisania
Cancel = anuluje poprzednie żądanie

Razem = wysyłasz tylko 1 request, dopiero gdy użytkownik przestał pisać! 🎯

import { useState, useEffect } from 'react';
import axios from 'axios';

function UserSearch() {
  const [query, setQuery] = useState('');
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const cancelTokenSource = axios.CancelToken.source();

    async function searchUsers() {
      if (!query) {
        setUsers([]);
        return;
      }

      try {
        const response = await apiClient.get('/users/search', {
          params: { q: query },
          cancelToken: cancelTokenSource.token,
        });
        setUsers(response.data);
      } catch (error) {
        if (!axios.isCancel(error)) {
          console.error('Search error:', error);
        }
      }
    }

    const timeoutId = setTimeout(searchUsers, 500); // Debounce

    return () => {
      clearTimeout(timeoutId);
      cancelTokenSource.cancel(); // Anuluj przy unmount
    };
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Szukaj użytkowników..."
      />
      <ul>
        {users.map((user: any) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}
🌟 Przykład: Co się dzieje krok po kroku
// Użytkownik wpisuje szybko: "J" → "Ja" → "Jan"

// t=0ms: Wpisuje "J"
setQuery('J')
→ useEffect uruchamia się
→ Ustawia timeout 500ms
→ Ustawia cancel token

// t=100ms: Wpisuje "Ja" 
setQuery('Ja')
→ useEffect uruchamia się PONOWNIE
→ cleanup() z poprzedniego useEffect:
  ✅ Anuluje timeout dla "J"
  ✅ Anuluje cancel token dla "J"
→ Ustawia NOWY timeout 500ms
→ Ustawia NOWY cancel token

// t=200ms: Wpisuje "Jan"
setQuery('Jan')
→ useEffect uruchamia się PONOWNIE
→ cleanup() z poprzedniego useEffect:
  ✅ Anuluje timeout dla "Ja"
  ✅ Anuluje cancel token dla "Ja"
→ Ustawia NOWY timeout 500ms
→ Ustawia NOWY cancel token

// t=700ms: Timeout się kończy!
→ Wysyła request dla "Jan"
→ Dostaje wyniki
→ Pokazuje użytkownikowi

// WYNIK: Tylko 1 request! ✨
Retry logic (ponowna próba)

Automatyczne ponowienie żądania przy błędzie.

🔍 Po co retry?

Czasami request failuje z przyczyn tymczasowych:

  • ❌ Użytkownik miał krótką przerwę w internecie (Wi-Fi restart)
  • ❌ Serwer był przeciążony (500 Server Error)
  • ❌ Timeout - zapytanie trwało za długo

Zamiast pokazać błąd użytkownikowi, spróbuj ponownie automatycznie! Często za 2. razem działa! 🔄

Exponential backoff = czekaj coraz dłużej między próbami (1s → 2s → 4s...). Nie bombarduj serwera!

async function fetchWithRetry<T>(
  fetchFn: () => Promise<T>,
  maxRetries: number = 3,
  delay: number = 1000
): Promise<T> {
  let lastError: any;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetchFn();
    } catch (error) {
      lastError = error;
      console.log(`Attempt ${i + 1} failed, retrying...`);
      
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); // Exponential backoff
      }
    }
  }

  throw lastError;
}

// Użycie
const users = await fetchWithRetry(() => apiClient.get('/users'));

Axios retry interceptor

🔍 axios-retry - automatyczny retry dla Axios

Biblioteka axios-retry dodaje retry automatycznie do WSZYSTKICH requestów! Nie musisz owijać każdego żądania w fetchWithRetry.

npm install axios-retry
import axiosRetry from 'axios-retry';

axiosRetry(apiClient, {
  retries: 3, // Liczba prób
  retryDelay: axiosRetry.exponentialDelay, // Wykładniczy delay (1s, 2s, 4s...)
  retryCondition: (error) => {
    // Retry tylko dla błędów sieciowych lub 5xx
    return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
           error.response?.status >= 500;
  },
});
🌟 Przykład: Konfiguracja retry dla różnych endpointów
import axiosRetry from 'axios-retry';

// Dla krytycznych endpointów - więcej prób
axiosRetry(apiClient, {
  retries: 5,
  retryDelay: axiosRetry.exponentialDelay,
  retryCondition: (error) => {
    const config = error.config;
    
    // Krytyczne endpointy - retry zawsze (oprócz 4xx)
    if (config?.url?.includes('/payment') || config?.url?.includes('/checkout')) {
      return error.response?.status !== 400; // Nie retry dla bad request
    }
    
    // Dla reszty - tylko network errors i 5xx
    return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
           (error.response?.status ?? 0) >= 500;
  },
  
  onRetry: (retryCount, error, requestConfig) => {
    console.log(`Retry ${retryCount} for ${requestConfig.url}`);
    
    // Pokaż toast tylko przy ostatniej próbie
    if (retryCount === 5) {
      toast.warning('Problemy z połączeniem. Próbuję ponownie...');
    }
  }
});
Praktyczny przykład: API service

🔍 Service Pattern - dlaczego warto?

Bez service - API calls rozrzucone po komponentach:

// UserList.tsx
axios.get('/users');

// UserProfile.tsx  
axios.get('/users/1');

// UserEdit.tsx
axios.put('/users/1', data);

Z service - wszystko w jednym miejscu:

// services/userService.ts
export const userService = {
  getAll: () => api.get('/users'),
  getById: (id) => api.get(`/users/${id}`),
  update: (id, data) => api.put(`/users/${id}`, data)
};

// Komponenty używają service
import { userService } from '../services/userService';
userService.getAll();
userService.getById(1);

Korzyści:

  • ✅ DRY - nie powtarzasz URLi
  • ✅ Łatwe zmiany - zmienisz endpoint w 1 miejscu
  • ✅ Typowane - TypeScript wie jakie dane dostaniesz
  • ✅ Testowalne - łatwo mockować service
// services/userService.ts
import { apiClient } from '../api/client';

export interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
  role: 'user' | 'admin';
}

export interface CreateUserDto {
  name: string;
  email: string;
  password: string;
}

export interface UpdateUserDto {
  name?: string;
  email?: string;
  avatar?: string;
}

class UserService {
  private readonly basePath = '/users';

  async getAll(): Promise<User[]> {
    const response = await apiClient.get<{ data: User[] }>(this.basePath);
    return response.data.data;
  }

  async getById(id: number): Promise<User> {
    const response = await apiClient.get<{ data: User }>(`${this.basePath}/${id}`);
    return response.data.data;
  }

  async create(userData: CreateUserDto): Promise<User> {
    const response = await apiClient.post<{ data: User }>(this.basePath, userData);
    return response.data.data;
  }

  async update(id: number, userData: UpdateUserDto): Promise<User> {
    const response = await apiClient.put<{ data: User }>(
      `${this.basePath}/${id}`,
      userData
    );
    return response.data.data;
  }

  async delete(id: number): Promise<void> {
    await apiClient.delete(`${this.basePath}/${id}`);
  }

  async uploadAvatar(id: number, file: File): Promise<string> {
    const formData = new FormData();
    formData.append('avatar', file);

    const response = await apiClient.post<{ data: { url: string } }>(
      `${this.basePath}/${id}/avatar`,
      formData,
      {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      }
    );

    return response.data.data.url;
  }
}

export const userService = new UserService();

Użycie w komponencie:

import { userService } from '../services/userService';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadUsers() {
      try {
        const data = await userService.getAll();
        setUsers(data);
      } catch (error) {
        toast.error('Nie udało się załadować użytkowników');
      } finally {
        setLoading(false);
      }
    }

    loadUsers();
  }, []);

  if (loading) return <div>Ładowanie...</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
💪 Ćwiczenie 3: API dla bloga

Stwórz kompletny service dla bloga:

  1. PostService:
    • getAll(page?, limit?) - z paginacją
    • getById(id)
    • create(post)
    • update(id, post)
    • delete(id)
    • search(query) - z debounce i cancel
  2. CommentService:
    • getByPostId(postId)
    • create(postId, comment)
    • delete(commentId)
  3. Wszystko typowane (interfaces)
  4. Obsługa błędów w każdej metodzie
Best Practices

1. Używaj instancji Axios

// ✅ Dobrze
const apiClient = axios.create({ baseURL: '/api' });

// ❌ Źle
axios.get('https://api.example.com/users');

2. Typuj wszystko

// ✅ Dobrze
interface User { id: number; name: string; }
const user = await apiClient.get<User>('/user/1');

// ❌ Źle
const user = await apiClient.get('/user/1'); // any

3. Centralizuj API calls

// ✅ Dobrze - services/userService.ts
export const userService = {
  getAll: () => apiClient.get('/users'),
  getById: (id) => apiClient.get(`/users/${id}`),
};

// ❌ Źle - API calls w komponentach
axios.get('/users');

4. Globalna obsługa błędów

// ✅ Interceptor dla wszystkich błędów
apiClient.interceptors.response.use(
  response => response,
  error => {
    // Centralna obsługa
    handleApiError(error);
    return Promise.reject(error);
  }
);

5. Environment variables

// ✅ Używaj zmiennych środowiskowych
const API_URL = import.meta.env.VITE_API_URL;

// ❌ Hardcoded URL
const API_URL = 'https://api.example.com';
🌟 Przykład: Kompletna struktura API w projekcie
src/
├── api/
│   ├── client.ts              # Axios instance + interceptory
│   └── config.ts              # Konfiguracja (baseURL, timeout)
├── services/
│   ├── userService.ts         # User CRUD
│   ├── postService.ts         # Post CRUD
│   └── authService.ts         # Login, register, logout
├── types/
│   ├── user.ts                # User interface
│   ├── post.ts                # Post interface
│   └── api.ts                 # ApiResponse interface
└── hooks/
    ├── useUsers.ts            # Custom hook dla users
    └── usePosts.ts            # Custom hook dla posts

// api/client.ts
export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000,
});

// Interceptors
apiClient.interceptors.request.use(/* ... */);
apiClient.interceptors.response.use(/* ... */);

// services/userService.ts
import { apiClient } from '../api/client';
export const userService = { /* CRUD methods */ };

// hooks/useUsers.ts
import { userService } from '../services/userService';
export function useUsers() { /* useState + useEffect */ }

Czyste rozdzielenie odpowiedzialności (separation of concerns)! 🎯

Podsumowanie

To był praktyczny wpis o komunikacji z API! Nauczyliśmy się:

  • Fetch API – wbudowane API przeglądarek, podstawowe użycie
  • Axios – lepsza alternatywa z większą funkcjonalnością
  • Konfiguracja – tworzenie instance z baseURL i defaultami
  • TypeScript – typowanie requestów i responses
  • Interceptory – request/response interceptors, token, logging
  • Upload plików – FormData, progress tracking
  • Cancel requests – anulowanie żądań przy unmount
  • Retry logic – automatyczne ponowienie przy błędzie
  • Service pattern – organizacja API calls
  • Best Practices – 5 profesjonalnych wzorców

Komunikacja z API to fundament każdej aplikacji. Teraz masz kompletną wiedzę by profesjonalnie integrować frontend z backendem!

W kolejnym wpisie poznamy React Query (TanStack Query) – najlepszą bibliotekę do zarządzania stanem serwerowym, cache, refetch, mutations!

Zadanie dla Ciebie

Zintegruj swoją aplikację z API:

  1. Skonfiguruj Axios instance z baseURL
  2. Stwórz service dla głównego zasobu (users/posts/products)
  3. Dodaj request interceptor z tokenem
  4. Dodaj response interceptor z globalną obsługą błędów
  5. Zaimplementuj upload plików z progress bar
  6. (Bonus) Dodaj retry logic
🎯 BONUS: Wielki projekt końcowy - Kompletny API Layer

Stwórz profesjonalny API layer dla aplikacji e-commerce:

Struktura:

  • api/client.ts - Axios instance z interceptorami
  • services/productService.ts - CRUD dla produktów
  • services/cartService.ts - operacje na koszyku
  • services/orderService.ts - składanie zamówień
  • services/authService.ts - login/register/logout

Features:

  • ✅ Typowanie TypeScript (wszystkie interfejsy)
  • ✅ Request interceptor (token auth)
  • ✅ Response interceptor (błędy 401/403/500)
  • ✅ Retry logic (3 próby dla 5xx)
  • ✅ Cancel tokens (search z debounce)
  • ✅ Upload obrazów produktów (FormData + progress)
  • ✅ Environment variables (dev/prod)
  • ✅ Logging w development

Services:

  1. ProductService: getAll, getById, search, create, update, delete, uploadImage
  2. CartService: getCart, addItem, updateQuantity, removeItem, clear
  3. OrderService: createOrder, getOrders, getOrderById, cancelOrder
  4. AuthService: login, register, logout, refreshToken, getMe

To production-ready API layer! Po ukończeniu tego projektu będziesz mógł zintegrować frontend z DOWOLNYM backendem! 🚀✨