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).
await - czeka aż serwer odpowie (może trwać sekundy!)
response - obiekt z odpowiedzią serwera (status, headers, body)
response.json() - parsuje body jako JSON (też async!)
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
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;
}
}
// 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
// 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:
Zdefiniuj interface Post (id, title, content, authorId, createdAt)
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
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
Teraz każde żądanie automatycznie ma token w nagłówku!
🔍 Jak działa request interceptor krok po kroku:
Wywołujesz: apiClient.get('/users')
Axios zatrzymuje żądanie: "Czekaj! Przejdź przez interceptor"
Interceptor: Pobiera token z localStorage
Interceptor: Dodaje Authorization: Bearer abc123 do headers
Interceptor: Zwraca zmodyfikowany config
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):