Najlepsze biblioteki do pracy z API: porównanie dla programistów

0
28
3/5 - (1 vote)

Nawigacja:

Po co w ogóle używać bibliotek do API, skoro jest „goły” HTTP?

Rola warstwy klienckiej w architekturze

Komunikacja z API to dziś chleb powszedni niemal każdego projektu. Frontend łączy się z backendem, backend z zewnętrznymi usługami (płatności, mailing, CRM, chmura), mikroserwisy wymieniają dane między sobą. W każdym z tych miejsc pojawia się ta sama potrzeba: wysłać żądanie HTTP, odebrać odpowiedź, przełożyć ją na obiekty w kodzie i rozsądnie zareagować na błędy.

W najprostszym wariancie wystarczy natywne wywołanie HTTP – w JavaScript fetch, w Pythonie http.client, w Javie HttpURLConnection lub współczesne HttpClient. Na początku to kusi: „po co kolejna biblioteka?”. Problem w tym, że gdy tylko rośnie liczba endpointów, autoryzacja, retry czy logowanie, szybko okazuje się, że brakuje warstwy pośredniej. Tą warstwą jest właśnie klient API oparty o bibliotekę HTTP – spójne miejsce na zasady komunikacji z zewnętrznym światem.

Dobrze zaprojektowany klient API jest jak adaptor do gniazdka na lotnisku: zaszywa w sobie wszystkie różnice, standardy i „dziwactwa” zewnętrznego systemu. Reszta aplikacji korzysta z jednego, przewidywalnego interfejsu: metod typu getUser(), createOrder(), listInvoices(). Pod spodem dzieje się HTTP, nagłówki, serializacja JSON, ale w kodzie domenowym tego nie widać. Z czasem to robi ogromną różnicę dla czytelności i odporności projektu na zmiany.

Typowe problemy przy ręcznej obsłudze żądań

Ręczne wołanie HTTP na „gołych” prymitywach szybko prowadzi do powielania tych samych fragmentów. Każde wywołanie musi:

  • dodać te same nagłówki (np. Authorization, Content-Type, Accept),
  • obsłużyć serializację i deserializację JSON lub innego formatu,
  • sprawdzić kod statusu i różnicować zachowanie dla 2xx / 4xx / 5xx,
  • zająć się timeoutami, retry, ewentualnie backoffem,
  • zalogować żądanie i odpowiedź w razie problemów.

Bez warstwy abstrakcji kończy się to kopiowaniem tych bloków po całym projekcie. Gdy trzeba zmienić nagłówek albo domyślny timeout, zaczyna się „polowanie” po plikach. Do tego dochodzi brak spójnego sposobu obsługi wyjątków: w jednym miejscu rzucasz wyjątek przy kodzie 404, w innym zwracasz null, a w jeszcze innym cicho ignorujesz błąd. Po kilku miesiącach nikt nie jest pewny, która część zachowuje się jak.

Pojawia się też ryzyko drobnych, lecz groźnych błędów: zapomniany nagłówek, niezwolnione połączenie, brak timeoutu (zawieszające się wątki), literówki w URL-ach. Biblioteki do pracy z API często „pilnują” takich detali za ciebie – albo domyślnymi ustawieniami, albo wymuszając konkretny sposób konfiguracji.

Różnica na przykładzie: natywny HTTP kontra biblioteka

Dla porównania, prosty przykład w JavaScript. Najpierw „goły” fetch:

// pobranie użytkownika z API z użyciem "gołego" fetch
async function getUser(userId, token) {
  const response = await fetch(`https://api.example.com/users/${userId}`, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) {
    const body = await response.text();
    console.error('Request failed', response.status, body);
    throw new Error(`API error: ${response.status}`);
  }

  return await response.json();
}

Teraz ta sama funkcja z użyciem skonfigurowanego raz klienta Axios:

import axios from 'axios';

// konfiguracja wspólnego klienta
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: {
    'Accept': 'application/json'
  }
});

// interceptor dodający token automatycznie
apiClient.interceptors.request.use(config => {
  const token = getAuthTokenSomehow();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

async function getUser(userId) {
  const { data } = await apiClient.get(`/users/${userId}`);
  return data;
}

Różnica? Funkcja domenowa getUser jest czysta, krótka i skupia się na celu, a nie na szczegółach HTTP. Gdy dojdzie obsługa retry, limitów zapytań czy logowania – trafiają one głównie do konfiguracji apiClient, a nie do setek rozproszonych wywołań.

Kryteria wyboru biblioteki do pracy z API

Technologia, styl programowania i kontekst projektu

Pierwszy filtr jest trywialny, ale często ignorowany: język i środowisko. Inne narzędzia pasują do frontendu w przeglądarce, inne do backendu w Pythonie, jeszcze inne do aplikacji mobilnej. Tam, gdzie w ekosystemie powstał naturalny „standard de facto” (np. requests w Pythonie, Axios w JS), opłaca się iść z prądem – łatwiej znaleźć przykłady, wsparcie i nowych członków zespołu, którzy to już znają.

Liczy się też styl programowania:

  • czy projekt używa podejścia reaktywnego (np. Spring WebFlux, RxJava) czy raczej klasycznych wywołań blokujących,
  • czy stosujesz intensywnie async/await (JS, Python, Kotlin coroutines),
  • czy ważne jest ścisłe typowanie i generowanie kodu na bazie schematu (OpenAPI, GraphQL schema).

Kontekst architektury również dużą rolę odgrywa. W mikroserwisach przyda się integracja z service discovery, circuit breakerem, limitami zapytań. W aplikacji serverless (np. AWS Lambda, Cloud Functions) istotne będą krótki czas „zimnego startu” i brak zbędnych zależności. W frontendzie liczy się wielkość bundla i to, czy biblioteka działa w przeglądarce bez kombinacji.

Parametry techniczne i „ludzkie”

Drugą grupą kryteriów są parametry techniczne i – co równie ważne – „ludzkie” aspekty bibliotek. Przy wyborze klienta API warto przejść przez krótką check‑listę:

  • Wydajność i zużycie zasobów – czy biblioteka jest lekka, jak radzi sobie z dużą liczbą równoległych połączeń, czy wspiera keep‑alive, HTTP/2.
  • Dojrzałość – od jak dawna istnieje, czy API jest stabilne, czy wersje nie łamią kompatybilności co kwartał.
  • Popularność – ile ma gwiazdek, pobrań, jak często pojawiają się commity; nie chodzi o pogoń za trendem, tylko o minimalne poczucie, że projekt nie jest „martwy”.
  • Ergonomia API – czy kod z tą biblioteką czyta się naturalnie, czy dobrze składa się z resztą twojego stosu (np. integracja z DI, loggerem, systemem konfiguracji).
  • Obsługa błędów – czy biblioteka wymusza rozsądne timeouty, czy daje wsparcie dla retry i backoff, czy błędy mają sensowną strukturę.
  • Testowalność – wsparcie dla mocków, możliwość wstrzyknięcia własnego transportu, integracja z narzędziami do testów integracyjnych.
  • Dokumentacja i społeczność – czy dokumentacja jest kompletna, czy istnieją blogi, kursy i artykuły wokół tej biblioteki.

Na koniec dochodzi jeszcze zgodność z konwencjami w danej społeczności. W Spring Boot powszechnie stosuje się RestTemplate (choć już przestarzały) i WebClient, w świecie Androida – Retrofit, w Pythona backendach – requests/httpx. Wybierając coś zupełnie egzotycznego, często ściąga się na siebie koszty, które nie mają uzasadnienia w zysku.

JavaScript/TypeScript – fetch, Axios, got i inni

Natywny fetch: plusy, minusy i brakujące elementy

fetch stał się standardowym mechanizmem HTTP w przeglądarkach i doczekał się wsparcia w Node.js. Jego zalety są oczywiste: jest wbudowany, prosty i oparty na Promise’ach, więc współgra z async/await. Dla małych projektów lub pojedynczych żądań to wygodna opcja.

Ma jednak kilka istotnych ograniczeń:

  • Brak domyślnego timeoutu – bez dodatkowej logiki wywołanie może „wisieć” bardzo długo, jeśli serwer nie odpowiada.
  • Obsługa błędówfetch nie rzuca wyjątku przy kodach 4xx/5xx, trzeba ręcznie sprawdzać response.ok i obsłużyć scenariusze błędów.
  • Brak interceptora – nie ma natywnego mechanizmu do globalnego przechwytywania żądań i odpowiedzi w celu modyfikacji nagłówków, logowania czy odświeżenia tokena.
  • Konfiguracja na żądanie – nie istnieje wbudowany mechanizm „globalnego klienta” z bazowym URL czy domyślnymi nagłówkami (tworzysz go sam, owijając fetch w swoją warstwę).

W większych projektach sprowadza się to do pisania własnych „mini‑klientów” – plików, które konstruują URL, doklejają token, sprawdzają błędy. W pewnym momencie sensowniejsze staje się sięgnięcie po bibliotekę, która już to robi i jest dobrze przetestowana.

Axios – klasyk w świecie frontendu i Node.js

Axios stał się w praktyce standardową biblioteką HTTP w JavaScript, szczególnie w React, Vue i Node.js. Popularność wynika z kilku cech, które prostują typowe problemy z fetch:

  • Domyślna obsługa JSON – wystarczy axios.get(url), a response.data ma już sparsowany JSON (o ile nagłówki na to wskazują).
  • Globalny klient – możliwość stworzenia instancji przez axios.create() z bazowym URL, nagłówkami, timeoutem.
  • Interceptory – funkcje wywoływane przed wysłaniem żądania i po otrzymaniu odpowiedzi.
  • Wsparcie dla starszych środowisk – Axios długo był wygodny tam, gdzie fetch nie był jeszcze dobrze wspierany.

Typowy wzorzec użycia w projekcie frontendowym wygląda tak:

import axios from 'axios';

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

api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      // np. odśwież token lub przekieruj na logowanie
      handleUnauthorized();
    }
    return Promise.reject(error);
  }
);

export async function getProfile() {
  const { data } = await api.get('/me');
  return data;
}

Dzięki interceptorom odświeżanie tokena, logowanie błędów czy modyfikacja nagłówków dzieje się w jednym miejscu. Pojedyncze funkcje API są wtedy krótkie i nie powielają tych samych fragmentów. Axios ułatwia także przesyłanie plików, konfigurację proxy w Node.js czy wysyłanie formularzy.

W TypeScript warto owinąć Axiosa własną warstwą, aby zachować statyczne typowanie odpowiedzi:

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

async function getUser(userId: string): Promise<User> {
  const { data } = await api.get<User>(`/users/${userId}`);
  return data;
}

Dzięki temu każda zmiana po stronie API (np. usunięcie pola) może zostać wychwycona przez TS, o ile generujesz lub utrzymujesz aktualne typy.

got, ky i lekkie alternatywy w Node.js

W środowisku Node.js pojawiły się nowocześniejsze alternatywy dla Axiosa, lepiej dopasowane do serwerowych zastosowań. Do najciekawszych należą got i ky.

got to bogata biblioteka HTTP dla Node.js, oferująca m.in.:

  • wbudowany retry i backoff,
  • obsługę HTTP/2,
  • pipeline hooków (podobnie jak middleware),
  • opcję streamowania odpowiedzi,
  • dobrą integrację z TypeScript.

Przykładowe użycie:

import got from 'got';

const client = got.extend({
  prefixUrl: 'https://api.example.com',
  responseType: 'json',
  retry: { limit: 3 },
  timeout: { request: 5000 }
});

type User = {
  id: string;
  email: string;
};

export async function getUser(userId: string): Promise<User> {
  const response = await client.get(`users/${userId}`);
  return response.body as User;
}

ky z kolei jest lekkim wrapperem na fetch, idealnym do przeglądarki i nowoczesnego frontendu. Oferuje prostszy interfejs niż czysty fetch (m.in. automatyczne parse’owanie JSONa i obsługę błędów), a przy tym ma minimalny narzut. Dla aplikacji frontendowych, gdzie każdy kilobajt bundla ma znaczenie, bywa atrakcyjną alternatywą dla Axiosa.

Mały klient API w TypeScript z typowaniem i obsługą błędów

Przykładowy, mały klient oparty na fetch z warstwą abstrakcji

Złożenie kilku klocków – fetch, własna obsługa błędów, typy – daje prosty, ale zaskakująco skuteczny klient API. To coś, co często ląduje w katalogu shared/api i jest używane w całej aplikacji.

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

export class ApiError extends Error {
  constructor(
    public status: number,
    public body: unknown,
    message?: string
  ) {
    super(message ?? `Request failed with status ${status}`);
  }
}

interface RequestOptions<TBody> {
  method?: HttpMethod;
  body?: TBody;
  signal?: AbortSignal;
  authToken?: string | null;
}

const BASE_URL = 'https://api.example.com';

async function request<TResponse, TBody = unknown>(
  path: string,
  options: RequestOptions<TBody> = {}
): Promise<TResponse> {
  const { method = 'GET', body, signal, authToken } = options;

  const headers: Record<string, string> = {
    'Accept': 'application/json'
  };

  const init: RequestInit = { method, headers, signal };

  if (body !== undefined) {
    headers['Content-Type'] = 'application/json';
    init.body = JSON.stringify(body);
  }

  if (authToken) {
    headers['Authorization'] = `Bearer ${authToken}`;
  }

  const response = await fetch(`${BASE_URL}${path}`, init);

  const contentType = response.headers.get('content-type') ?? '';
  const isJson = contentType.includes('application/json');
  const responseBody = isJson ? await response.json() : await response.text();

  if (!response.ok) {
    throw new ApiError(response.status, responseBody);
  }

  return responseBody as TResponse;
}

// Przykładowe „metody domenowe”

export interface User {
  id: string;
  email: string;
}

export async function getCurrentUser(token: string): Promise<User> {
  return request<User>('/me', { authToken: token });
}

export async function createUser(
  token: string,
  payload: { email: string; password: string }
): Promise<User> {
  return request<User, typeof payload>('/users', {
    method: 'POST',
    body: payload,
    authToken: token
  });
}

Taki moduł jest łatwy do przetestowania: funkcję request można otoczyć mockiem fetch w unit testach, a w testach integracyjnych podmienić BASE_URL na lokalny serwer. W większym projekcie ten wzorzec rozbudowuje się o interceptory, logowanie i mechanizmy odświeżania tokenów.

Python – requests, httpx i klienci generowani

requests – prostota, która wystarcza w 80% przypadków

W ekosystemie Pythona biblioteka requests jest trochę jak „kanoniczny” sposób na HTTP. Kod typu requests.get() pojawia się w dokumentacji, kursach i odpowiedziach na forach od lat – i nie bez powodu. API jest czytelne, a zachowanie przewidywalne.

import requests

BASE_URL = "https://api.example.com"


def get_user(user_id: str) -> dict:
    response = requests.get(f"{BASE_URL}/users/{user_id}", timeout=5)
    response.raise_for_status()
    return response.json()


def create_user(email: str, password: str) -> dict:
    payload = {"email": email, "password": password}
    response = requests.post(f"{BASE_URL}/users", json=payload, timeout=5)
    response.raise_for_status()
    return response.json()

Kilka rzeczy szczególnie pomaga w codziennej pracy:

  • timeout jest parametrem funkcji – nie trzeba ręcznie o tym pamiętać w warstwie niskopoziomowej.
  • response.raise_for_status() zamienia nieudane odpowiedzi na wyjątki, dzięki czemu błędy nie prześlizgują się niezauważone.
  • sesje (requests.Session) utrzymują nagłówki, połączenia keep‑alive i cookie między kolejnymi wywołaniami.

Przy integracji z zewnętrznym API sesje dają sporą oszczędność kodu. Zamiast przeklejać nagłówki, konfigurujesz je raz i używasz w wielu miejscach.

import requests

class ApiClient:
    def __init__(self, base_url: str, token: str | None = None) -> None:
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({"Accept": "application/json"})
        if token:
            self.session.headers["Authorization"] = f"Bearer {token}"

    def _url(self, path: str) -> str:
        return f"{self.base_url}/{path.lstrip('/')}"

    def get(self, path: str, **kwargs):
        return self.session.get(self._url(path), timeout=5, **kwargs)

    def post(self, path: str, **kwargs):
        return self.session.post(self._url(path), timeout=5, **kwargs)


client = ApiClient("https://api.example.com", token="...")

resp = client.get("/me")
resp.raise_for_status()
profile = resp.json()

requests nie jest jednak idealny do wszystkiego. Brakuje w nim wsparcia dla async/await, a obsługa HTTP/2 odbywa się raczej przez zewnętrzne dodatki niż natywnie.

httpx – nowoczesny następca z asynchronią i HTTP/2

httpx powstał jako „requests dla async świata”. API jest bardzo podobne, ale wspiera zarówno tryb synchroniczny, jak i asynchroniczny, obsługuje HTTP/2, a do tego lepiej integruje się z ekosystemem ASGI (FastAPI, Starlette).

import httpx

BASE_URL = "https://api.example.com"


async def get_user(user_id: str) -> dict:
    async with httpx.AsyncClient(base_url=BASE_URL, timeout=5) as client:
        response = await client.get(f"/users/{user_id}")
        response.raise_for_status()
        return response.json()

W praktyce często tworzy się jedną instancję AsyncClient w czasie życia aplikacji (np. w FastAPI jako dependency). To zmniejsza koszty nawiązywania połączeń i pozwala utrzymać wspólną konfigurację: nagłówki, retry, logowanie.

import httpx
from fastapi import Depends, FastAPI

app = FastAPI()

async_client: httpx.AsyncClient | None = None


async def get_client() -> httpx.AsyncClient:
    return async_client


@app.on_event("startup")
async def startup_event():
    global async_client
    async_client = httpx.AsyncClient(
        base_url="https://api.example.com",
        timeout=5.0,
    )


@app.on_event("shutdown")
async def shutdown_event():
    await async_client.aclose()


@app.get("/proxy-me")
async def proxy_me(client: httpx.AsyncClient = Depends(get_client)):
    resp = await client.get("/me")
    return resp.json()

httpx ma jeszcze jedną zaletę: bogaty system hooków i transportów. Można podmienić warstwę transportową na „fałszywy” backend w testach bez odpalania rzeczywistego serwera HTTP, co bardzo upraszcza testy integracyjne.

Statyczne typowanie z Pydantic i httpx: mały, bezpieczniejszy klient

Python sam z siebie nie wymusza typów odpowiedzi, ale połączenie httpx z Pydantic (lub Pydantic‑kompatybilnymi modelami) daje przyjemny balans między dynamiką a bezpieczeństwem.

from pydantic import BaseModel
import httpx


class User(BaseModel):
    id: str
    email: str


class ApiClient:
    def __init__(self, base_url: str, token: str | None = None) -> None:
        self._client = httpx.Client(base_url=base_url, timeout=5.0)
        self._client.headers["Accept"] = "application/json"
        if token:
            self._client.headers["Authorization"] = f"Bearer {token}"

    def get_user(self, user_id: str) -> User:
        resp = self._client.get(f"/users/{user_id}")
        resp.raise_for_status()
        return User.model_validate(resp.json())

Jeśli backend doda nowe wymagane pole, którego nie ma w modelu, walidacja rzuci wyjątek. Błąd wychodzi w testach (albo na logach), zamiast spokojnie „przeciec” do runtime’u.

Generowanie klientów z OpenAPI – swagger-codegen, openapi-generator, datamodel-code-generator

Gdy API ma opis w OpenAPI, ręczne pisanie klientów szybko staje się uciążliwe. Wtedy wchodzą w grę generatory kodu, które z definicji tworzą klienta i modele danych.

Popularne narzędzia w świecie Pythona to m.in.:

  • openapi-generator – wielojęzyczny generator, potrafi tworzyć klientów w Pythonie (sync i async, z różnymi bibliotekami HTTP pod spodem).
  • swagger-codegen – starszy projekt, wciąż używany w wielu firmach, ale powoli wypierany przez openapi‑generator.
  • datamodel-code-generator – generuje wyłącznie modele (np. Pydantic) na bazie schematu, resztę klienta można dopisać ręcznie.

Typowy przepływ pracy wygląda tak: backend wystawia openapi.json, pipeline CI uruchamia generator i nadpisuje katalog generated_client, a warstwa aplikacyjna używa już wygodnych metod typu client.get_user(user_id).

Kosztem jest zarządzanie wygenerowanym kodem (nie wszystko da się łatwo nadpisać) i konieczność dość „czystego” schematu OpenAPI. Gdy specyfikacja jest nieaktualna wobec rzeczywistego API, generator będzie produkował błędny kod – i tu nie ma magii, która to naprawi.

Zbliżenie ekranu komputera z kodem HTML, CSS i JavaScript
Źródło: Pexels | Autor: Саша Алалыкин

Java i Kotlin – Spring WebClient, Feign, Retrofit

Spring WebClient – reaktywny następca RestTemplate

W ekosystemie Spring Boot długo królował RestTemplate, ale dziś domyślnym wyborem staje się WebClient. Umożliwia zarówno styl reaktywny (Flux/Mono), jak i bardziej „imperatywny” z blokowaniem, a do tego dobrze integruje się z całą rodziną Springa.

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient apiWebClient(WebClient.Builder builder) {
        return builder
            .baseUrl("https://api.example.com")
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}
@Service
public class UserService {

    private final WebClient apiWebClient;

    public UserService(WebClient apiWebClient) {
        this.apiWebClient = apiWebClient;
    }

    public Mono<UserDto> getUser(String userId) {
        return apiWebClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserDto.class);
    }
}

WebClient ma rozbudowany system filtrów (filters), który pełni podobną rolę jak interceptory w Axiosie. Można tam dodać logowanie, metryki, obsługę tokenów czy retry.

@Bean
public WebClient apiWebClient(WebClient.Builder builder) {
    return builder
        .baseUrl("https://api.example.com")
        .filter((request, next) -> {
            long start = System.currentTimeMillis();
            return next.exchange(request)
                .doOnSuccess(response -> {
                    long duration = System.currentTimeMillis() - start;
                    log.info("{} {} -> {} ({} ms)",
                        request.method(), request.url(), response.statusCode(), duration);
                });
        })
        .build();
}

W aplikacjach, które i tak używają WebFlux, WebClient naturalnie „wpisuje się” w istniejący kod. W klasycznych aplikacjach MVC można go używać blokująco przez .block(), ale wtedy traci się część jego przewag, przede wszystkim związaną z charakterem reaktywnym.

Feign – deklaratywny klient HTTP w stylu Spring Cloud

Kolejnym popularnym narzędziem w świecie Springa jest Feign, a właściwie Spring Cloud OpenFeign. Pozwala opisywać wywołania HTTP adnotacjami na interfejsie – bardzo przypomina to definicję kontrolera, tylko że po stronie klienta.

@FeignClient(name = "usersClient", url = "${users.service.url}")
public interface UsersClient {

    @GetMapping("/users/{id}")
    UserDto getUser(@PathVariable("id") String id);

    @PostMapping("/users")
    UserDto createUser(@RequestBody CreateUserRequest request);
}

Taki interfejs jest potem wstrzykiwany jak zwykły bean Springowy, a developer wywołuje go jak lokalny serwis. Pod spodem działa HTTP, ale większość szczegółów konfiguruje się centralnie (time‑outy, retry, balancer, auth). To szczególnie dobrze pasuje do mikroserwisów w Spring Cloud, gdzie Feign integruje się z service discovery (Eureka, Consul) i load balancingiem.

Minusem jest to, że „magia” adnotacji może zaciemnić, jakie dokładnie nagłówki i parametry wysyła klient, zwłaszcza gdy projekt rośnie i pojawiają się globalne interceptory.

Retrofit – ulubieniec Androida i nie tylko

W świecie Androida słowo „HTTP klient” niemal automatycznie kojarzy się z Retrofitem. To biblioteka stworzona przez Square, działająca na bazie OkHttp. Podobnie jak Feign opiera się na interfejsach z adnotacjami, ale lepiej przystaje do idiomów Kotlinowych i mobilnego świata.

interface UsersApi {

    @GET("users/{id}")
    suspend fun getUser(
        @Path("id") id: String
    ): UserDto

    @POST("users")
    suspend fun createUser(
        @Body request: CreateUserRequest
    ): UserDto
}
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(MoshiConverterFactory.create())
    .build()

val usersApi = retrofit.create(UsersApi::class.java)

Wsparcie dla suspend funkcji sprawia, że wywołanie wygląda jak zwykła funkcja, a pod spodem Retrofit korzysta z korutyn. Do tego łatwo podmienić ConverterFactory (Moshi, Gson, Kotlinx Serialization) i dodać interceptory OkHttp do logowania, cache czy autoryzacji.

Bardzo częsty wzorzec to stworzenie warstwy „repository”, która używa Retrofitowego API i mapuje DTO na modele domenowe. Dzięki temu ewentualna migracja z Retrofit na coś innego nie przebija się wyżej niż warstwa danych.

WebClient/Retrofit w Kotlinie – spójne użycie korutyn i typów

Kotlin w parze z WebClientem czy Retrofitem pozwala pisać kod HTTP w stylu funkcyjnym i silnie typowanym. Zamiast przekazywać mapy i ręczne parse’owanie JSON‑a, częściej definiuje się data‑klasy i używa bibliotek typu Kotlinx Serialization lub Moshi.

Typizacja, błędy i testowalność w JVM‑owych klientach

W świecie JVM wygoda korzystania z WebClienta, Feigna czy Retrofit mocno zależy od tego, jak potraktujesz typy i obsługę błędów. Gdy wszystko sprowadza się do String i ręcznego parsowania JSON‑a, każda zmiana kontraktu API staje się loterią. Dużo zdrowiej oprzeć się na DTO z adnotacjami (Jackson, Moshi, Kotlinx Serialization) i zamkniętych typach błędów.

data class ErrorResponse(
    val code: String,
    val message: String
)

sealed class ApiResult<out T> {
    data class Success<T>(val value: T) : ApiResult<T>()
    data class Failure(val error: ErrorResponse, val status: Int) : ApiResult<Nothing>()
}

Taki ApiResult można spiąć z Retrofitowym CallAdapterFactory albo z WebClientem, mapując odpowiedzi HTTP na typy domenowe. W efekcie nie ma już „magicznych” wyjątków, tylko jawny, typowany wynik, który wymusza obsługę błędów w kodzie serwisów.

Do testów integracyjnych w JVM dobrze sprawdza się MockWebServer (Square) lub WireMock. Pozwalają one zasymulować zewnętrzne API bez stawiania prawdziwego serwera:

val server = MockWebServer()
server.enqueue(
    MockResponse()
        .setResponseCode(200)
        .setBody("""{"id":"123","email":"user@example.com"}""")
)

// retrofit / webclient wskazuje na server.url("/")

Takie podejście szybko ujawnia niezgodności między oczekiwanym a rzeczywistym JSON‑em – jeszcze zanim cokolwiek trafi na środowisko testowe.

Go – net/http, resty i generowane klienty

Go przychodzi z całkiem porządnym net/http w standardowej bibliotece, więc w wielu zespołach dodatkowe biblioteki przez długi czas w ogóle się nie pojawiają. Z czasem, gdy liczba wywołań rośnie, a logika obsługi błędów i retry zaczyna się powtarzać, wygodniej oprzeć się na czymś wyżej poziomowym.

„Goły” net/http – prosty, ale wymagający dyscypliny

Podstawowa kombinacja to http.Client + http.NewRequest + json.Decoder. Minimalistyczna, przejrzysta, ale też dość niskopoziomowa:

type User struct {
    ID    string `json:"id"`
    Email string `json:"email"`
}

func GetUser(client *http.Client, baseURL, id string) (*User, error) {
    req, err := http.NewRequest("GET", baseURL+"/users/"+id, nil)
    if err != nil {
        return nil, err
    }

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }

    var u User
    if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
        return nil, err
    }
    return &u, nil
}

Przy jednym czy dwóch endpointach to wystarcza. Gdy klienci do pięciu różnych serwisów zaczynają kopiować te same sprawdzanie statusu, logowanie, retry, trace ID – robi się miejsce na dedykowaną bibliotekę.

resty – „baterie w zestawie” dla HTTP w Go

Resty to najpopularniejszy „wysokopoziomowy” klient HTTP w Go. Oferuje wbudowane retry, obsługę JSON‑a, middleware’y (hooki przed i po wywołaniu), a przy tym nadal bazuje na net/http pod spodem.

client := resty.New().
    SetBaseURL("https://api.example.com").
    SetHeader("Accept", "application/json").
    SetRetryCount(3)

type User struct {
    ID    string `json:"id"`
    Email string `json:"email"`
}

var user User
resp, err := client.R().
    SetPathParam("id", "123").
    SetResult(&user).
    Get("/users/{id}")

Zamiast ręcznie dekodować JSON, wystarczy SetResult. Resty ma też hooki, które pełnią funkcję interceptorów:

client.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
    log.Printf("Calling %s %s", r.Method, r.URL)
    return nil
})

client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
    log.Printf("Response %d in %s", r.StatusCode(), r.Time())
    return nil
})

Kod biznesowy zostaje czystszy, a cross‑cutting concerns lądują w jednym miejscu.

Generowane klienty w Go – openapi‑generator, oapi‑codegen

Z uwagi na silne typowanie i brak wyjątków, Go świetnie łączy się z generowanymi klientami na bazie OpenAPI. Popularne narzędzia to:

  • openapi‑generator – generuje pełnych klientów i modele w Go, obsługując różne style API.
  • oapi‑codegen – mocno „go‑idiomatyczne” narzędzie, które potrafi generować zarówno serwer, jak i klienta.

W typowym projekcie backend udostępnia openapi.yaml, a oapi-codegen buduje interfejs klienta oraz struktury danych. Dzięki temu kontrakt API jest jednym źródłem prawdy, a zmiana typu pola od razu „psuje” kompilację w miejscach, które nie zostały zaktualizowane.

Rust – reqwest, Surf i generowane SDK

Rust mocno premiuje bezpieczeństwo i typy, więc praca z HTTP bywa nieco bardziej „ceremonialna”, ale w zamian zyskuje się wysoką pewność co do poprawności kodu. Klienci do API naturalnie łączą się tu z serde i asynchronicznym runtime’em (tokio lub async-std).

reqwest – standard de facto w Rust

Reqwest to najczęściej wybierana biblioteka HTTP w Rust. Ma API stylizowane na „builder pattern” i wspiera zarówno async, jak i tryb blokujący.

use reqwest::Client;
use serde::Deserialize;

#[derive(Deserialize)]
struct User {
    id: String,
    email: String,
}

async fn get_user(client: &Client, base_url: &str, id: &str) -> reqwest::Result<User> {
    let url = format!("{}/users/{}", base_url, id);
    let resp = client
        .get(url)
        .header("Accept", "application/json")
        .send()
        .await?
        .error_for_status()?;

    let user = resp.json::<User>().await?;
    Ok(user)
}

error_for_status() zamienia nie‑2xx statusy w błędy, więc obsługa wyjątków HTTP jest spójna. W większych projektach buduje się cienką warstwę „service client”, która grupuje wszystkie endpointy i dodaje wspólne nagłówki, logowanie czy retry.

Surf i ekosystem async‑std

Jeśli projekt bazuje na async-std, częściej pojawia się Surf. Skupia się na prostocie i czytelnych wywołaniach:

use serde::Deserialize;
use surf::Client;

#[derive(Deserialize)]
struct User {
    id: String,
    email: String,
}

async fn get_user(client: &Client, base_url: &str, id: &str) -> surf::Result<User> {
    let mut res = client
        .get(format!("{}/users/{}", base_url, id))
        .recv_json::<User>()
        .await?;
    Ok(res)
}

Surf celuje w minimalizm, więc jeśli potrzebne są zaawansowane funkcje (np. HTTP/2, fine‑grained time‑outy, connection pooling), częściej wygrywa reqwest.

Generowane klienci w Rust – openapi‑generator, ory‑client‑tools

Rustowe SDK wygodnie powstają na bazie specyfikacji OpenAPI. openapi‑generator ma tryb rustowy, który generuje typy z serde i klienta opartego na reqwest. Są też bardziej wyspecjalizowane narzędzia używane przez konkretne projekty (np. w ekosystemie ORY czy Kubernetes), ale idea jest ta sama: schemat definiuje wszystko.

W praktyce takie SDK często są publikowane jako crate na crates.io, a aplikacje konsumują już gotowego klienta. Zmiana wersji API sprowadza się wtedy do podbicia wersji dependency i poprawienia błędów kompilacji – zamiast ręcznego szukania miejsc, które „rozjechały się” z backendem.

Specjalistyczne biblioteki dla REST, GraphQL i gRPC

HTTP to tylko transport. Same API różnią się jednak stylem: klasyczny REST, GraphQL, gRPC, SSE, WebSockety. Każdy z tych światów dorobił się osobnych bibliotek klienckich i wzorców użycia.

REST – od „gołego” HTTP do bogatych SDK

REST opiera się na standardowych metodach HTTP, więc technicznie wystarczy dowolna biblioteka HTTP. W praktyce dochodzą konwencje: paginacja, filtry, etagi, caching, wersjonowanie. Specjalistyczne SDK często zamykają te mechanizmy w prostszych metodach:

  • stripe‑python / stripe‑java / stripe‑go – klient do API Stripe, który ukrywa szczegóły paginacji, idempotentnych requestów i webhooków.
  • aws‑sdk‑* – rozbudowane biblioteki do API AWS, pilnujące podpisywania żądań (Signature V4), retry z backoffem, credentiali.

Tam, gdzie dostawca chmury lub SaaS udostępnia oficjalne SDK, pisanie własnego klienta nad „gołym” HTTP to niemal zawsze niepotrzebna praca. Wyjątkiem są bardzo ograniczone środowiska (np. Serverless z ciasnym limitem rozmiaru paczki) lub nietypowe wymagania.

GraphQL – Apollo, urql, Relay i klienci generowani

GraphQL wymusza nieco inne podejście niż REST. Zamiast wielu endpointów jest pojedynczy, ale to zapytania definiują kształt odpowiedzi. Klient musi więc dobrze ogarniać cachowanie, normalizację danych i synchronizację stanu UI.

W świecie JavaScript dominują:

  • Apollo Client – rozbudowany klient z cache, subskrypcjami, obsługą błędów i integracją z Reactem, Vue, Angular.
  • urql – lżejszy, modułowy klient, pozwalający dobrać tylko potrzebne „exchange” (np. cache, retry, auth).
  • Relay – mocno typowany, zorientowany na wydajność klient od Facebooka, ale wymagający dyscypliny i narzędzi build.

Do tego dochodzą narzędzia generujące typy i hooki na podstawie schematu i zapytań, jak GraphQL Code Generator. W połączeniu z TypeScriptem daje to bardzo silne typowanie: jeśli backend zmieni nazwę pola w schemacie, build frontendu od razu się wywraca.

# przykład fragmentu konfiguracji codegen.yml
schema: https://api.example.com/graphql
documents: src/**/*.graphql
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"

W rezultacie w kodzie Reacta pracuje się na gotowych, typowanych hookach typu useGetUserQuery, zamiast składać zapytania ręcznie jako stringi.

gRPC – klienci z plików .proto i kanały po HTTP/2

gRPC działa na HTTP/2 i Protobuffers, więc wymaga zupełnie innych bibliotek niż klasyczny REST. Klienci są zazwyczaj generowani z plików .proto i obsługują zarówno wywołania unary, jak i streaming w obu kierunkach.

Przykładowo w Go proces wygląda tak:

// user.proto
service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse) {}
}

message GetUserRequest {
  string id = 1;
}

message UserResponse {
  string id = 1;
  string email = 2;
}
// komenda generująca klienta
protoc --go_out=. --go-grpc_out=. user.proto

Wygenerowany klient ma już gotowe metody typu GetUser(ctx, &GetUserRequest{Id: "123"}). Ręcznie konfiguruje się głównie kanał (grpc.Dial) i cross‑cutting concerns (interceptory, auth, retry). Podobnie jest w Javie, Pythonie czy C# – główny wysiłek to ogarnięcie kontraktu w .proto, a nie samego kodu klienta.

Wspólne wzorce niezależnie od języka i biblioteki

Narzędzie może się zmieniać – fetch, Axios, requests, WebClient, Resty – ale kilka praktyk wraca w każdym projekcie, który ma przetrwać dłużej niż jedną iterację.

Cienka warstwa klienta i mapowanie na domenę

Niezależnie od stosu technicznego dobrym refleksem jest stworzenie cienkiej warstwy „API client”, a dopiero nad nią budowanie logiki domenowej. Klient zna URL‑e, nagłówki, protokół i formaty, natomiast warstwa serwisów operuje na pojęciach biznesowych i nie przejmuje się tym, czy dane przyszły z REST, gRPC czy pliku.

Prosty przykład w TypeScript:

interface UserDto {
  id: string;
  email: string;
}

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

class UsersApiClient {
  constructor(private http: AxiosInstance) {}

  async getUser(id: string): Promise<UserDto> {
    const { data } = await this.http.get<UserDto>(`/users/${id}`);
    return data;
  }
}

class UserService {
  constructor(private api: UsersApiClient) {}

  async getUser(id: string): Promise<User> {
    const dto = await this.api.getUser(id);
    return { id: dto.id, email: dto.email };
  }
}

Migracja z Axios na fetch, z REST na GraphQL czy z jednego dostawcy płatności na innego ogranicza się wtedy do dolnej warstwy. Kod domenowy nawet „nie wie”, że coś pod spodem się zmieniło.

Konfiguracja w jednym miejscu: time‑outy, retry, auth

Najczęściej zadawane pytania (FAQ)

Po co używać bibliotek HTTP, skoro mogę wysyłać „gołe” żądania HTTP?

Przy jednym czy dwóch endpointach „goły” HTTP rzeczywiście wystarcza. Problem zaczyna się wtedy, gdy dochodzą kolejne punkty końcowe, autoryzacja, retry, logowanie czy obsługa błędów. Wtedy ten sam kod zaczynasz kopiować po całym projekcie, a każda zmiana nagłówka czy timeoutu zamienia się w polowanie po plikach.

Biblioteka HTTP pozwala zbudować spójnego klienta API – jedno miejsce, w którym ustawiasz nagłówki, serializację, obsługę błędów, retry czy logowanie. Reszta aplikacji woła proste metody domenowe typu getUser() czy createOrder() i nie interesuje się szczegółami protokołu. To jak posiadanie jednego „adaptera” do wszystkich gniazdek zamiast zestawu kabli i przejściówek rozsianych po domu.

Jaką bibliotekę do pracy z API wybrać w JavaScript/TypeScript: fetch czy Axios?

fetch ma tę zaletę, że jest wbudowany (przeglądarka, Node.js), prosty i oparty o Promise, więc dobrze gra z async/await. Nadaje się do małych projektów, prostych integracji i sytuacji, gdy naprawdę chcesz minimalnej liczby zależności. Trzeba jednak samemu dopisać timeouty, jednolitą obsługę błędów czy powtarzalną konfigurację nagłówków.

Axios daje z kolei warstwę „nad” HTTP: globalną konfigurację klienta, interceptory (np. automatyczne dorzucanie tokena), wygodną obsługę JSON, możliwość łatwego wpięcia retry czy logowania. Przy średnich i dużych aplikacjach kod oparty na Axiosie jest zwykle krótszy, bardziej spójny i łatwiejszy w utrzymaniu. Dobry kompromis to: małe skrypty – fetch, większe SPA/Node – Axios lub inna dedykowana biblioteka.

Jakie są typowe problemy przy ręcznym wywoływaniu HTTP bez biblioteki?

Najczęstszy kłopot to duplikacja logiki: wszędzie ręcznie dodajesz te same nagłówki, parsujesz JSON, sprawdzasz status odpowiedzi, ustawiasz timeouty i logujesz błędy. W jednym miejscu reagujesz na błąd 404 wyjątkiem, w innym zwracasz null, a w jeszcze innym po prostu go ignorujesz. Po kilku miesiącach nikt nie wie, czego się spodziewać po danym wywołaniu.

Dochodzi do tego cała gama „drobnych” błędów: brak timeoutu (wątki wiszą w nieskończoność), literówki w URL, zapomniane nagłówki, źle zwolnione połączenia. Biblioteki HTTP często mają sensowne domyślne ustawienia i wymuszają bardziej uporządkowaną konfigurację, dzięki czemu mniej rzeczy „da się popsuć” przypadkiem.

Jakie kryteria brać pod uwagę przy wyborze biblioteki do API?

Na początek spójrz na język i środowisko (frontend, backend, mobile, serverless) oraz styl programowania: czy używasz intensywnie async/await, programowania reaktywnego, czy potrzebujesz ścisłego typowania i generowania kodu z OpenAPI/GraphQL. To wstępnie odsieje narzędzia, które zupełnie nie pasują do Twojego stosu.

Później dochodzą kryteria techniczne i „ludzkie”: dojrzałość i popularność projektu, ergonomia API (czy kod się dobrze czyta), obsługa błędów i timeoutów, wydajność, testowalność (mocki, podmiana transportu), a także dokumentacja i społeczność. Często sensownym wyborem jest „standard de facto” w danym ekosystemie, bo łatwiej o przykłady, wsparcie i nowych ludzi w zespole.

Czy w małym projekcie naprawdę potrzebuję osobnej biblioteki HTTP?

Na samym początku nie zawsze. Mały skrypt, jedno wywołanie do API – natywny fetch w JS czy prosta funkcja w Pythonie rzeczywiście może wystarczyć. Kiedy jednak widzisz, że liczba endpointów rośnie, pojawia się autoryzacja, różne typy błędów i kilka serwisów zewnętrznych, opłaca się zatrzymać i zbudować cienką warstwę klienta API.

To trochę jak z „na szybko” napisanym SQL-em w każdym kontrolerze – działa, dopóki nie trzeba nic zmienić. Jeden wspólny klient HTTP na starcie oszczędza później wiele godzin porządków, kiedy projekt przestaje być „mały”.

Dlaczego niektóre biblioteki HTTP są „standardem de facto” i czy warto się nimi przejmować?

Takie biblioteki jak Axios w JS, requests/httpx w Pythonie czy Retrofit/OkHttp w Androidzie stały się standardem, bo przez lata dobrze rozwiązywały realne problemy i mają dużą społeczność. Dzięki temu łatwo znaleźć przykłady, odpowiedzi na Stack Overflow, gotowe pluginy, a wielu programistów zna je z poprzednich projektów.

W praktyce oznacza to mniejszą krzywą uczenia dla zespołu i mniejsze ryzyko, że projekt nagle „umrze”. Oczywiście można wybrać coś bardziej egzotycznego, ale wtedy dobrze mieć bardzo konkretny powód – w przeciwnym razie zyski z takiej decyzji są zwykle mniejsze niż koszty.

Czym różni się prosty klient API od „surowego” wywołania HTTP w kodzie?

Prosty klient API to osobna warstwa: masz np. moduł userApi z metodą getUser(id), która zwraca od razu obiekt domenowy, a nie surową odpowiedź HTTP. Pod spodem klient zajmuje się URL-ami, nagłówkami, tokenami, timeoutami, błędami i logowaniem. Kod biznesowy woła tylko „daj użytkownika” i nie interesuje się protokołem.

Przy „surowym” HTTP każdy fragment aplikacji musi znać wszystkie szczegóły techniczne (adresy endpointów, nagłówki, format odpowiedzi). To zwiększa szumy w kodzie i utrudnia zmiany – np. migrację z jednego API na inne czy modyfikację sposobu autoryzacji.

Najważniejsze punkty

  • Gołe wywołania HTTP szybko przestają wystarczać – przy większej liczbie endpointów, autoryzacji, retry czy logowaniu brak wspólnej warstwy klienckiej prowadzi do chaosu i trudnych w utrzymaniu „sklejanek” w kodzie.
  • Dedykowany klient API działa jak adapter do gniazdka na lotnisku: chowa w sobie formaty, nagłówki, dziwne statusy i konwencje zewnętrznego systemu, a reszcie aplikacji udostępnia proste, domenowe metody typu getUser() czy createOrder().
  • Ręczna obsługa HTTP sprzyja duplikacji logiki (nagłówki, serializacja, obsługa statusów, timeouty, logowanie), co kończy się „polowaniem po plikach”, gdy trzeba zmienić jedno globalne zachowanie, np. dodać nowy nagłówek czy skrócić timeout.
  • Brak spójnej warstwy abstrakcji powoduje niespójne reagowanie na błędy: w jednym miejscu 404 rzuca wyjątek, w innym zwraca null, a w jeszcze innym jest ignorowany – po kilku miesiącach nikt nie wie, jak system naprawdę reaguje na awarie.
  • Biblioteki HTTP pomagają unikać drobnych, ale kosztownych wpadek (zapomniany nagłówek, brak timeoutu, literówka w URL, niezwolnione połączenie), bo narzucają strukturę konfiguracji i dają sensowne domyślne ustawienia.
  • Skonfigurowany raz klient (jak Axios z interceptorami) pozwala utrzymać funkcje domenowe krótkie i czytelne – logika typu autoryzacja, retry czy logowanie siedzi w jednym miejscu zamiast w setkach wywołań rozrzuconych po projekcie.