Czym jest REST i skąd się wziął
REST (Representational State Transfer) to styl architektoniczny opisany w 2000 roku przez Roya Fieldinga w jego rozprawie doktorskiej. Fielding nie zaprojektował nowego protokołu — on nazwał i sformalizował wzorce, które i tak stały za projektem HTTP. REST nie jest więc biblioteką ani standardem W3C. To zbiór ograniczeń, które pozwalają budować skalowalne i luźno powiązane systemy rozproszone.
Fielding wymienił sześć ograniczeń: architekturę klient-serwer, bezstanowość (każde żądanie zawiera wszystko, co potrzebne do jego obsługi), cache’owalność, jednolity interfejs, warstwową architekturę oraz opcjonalne code on demand. W praktyce większość API, które dziś nazywamy „RESTowymi”, spełnia tylko część z nich — dlatego coraz częściej mówi się o pragmatycznym REST zamiast czystym podejściu Fieldinga.
Bezstanowość jest szczególnie istotna. Oznacza, że serwer nie przechowuje kontekstu sesji między kolejnymi żądaniami tego samego klienta — cała potrzebna informacja, łącznie z uwierzytelnieniem, musi być w każdym żądaniu. Dzięki temu możesz horyzontalnie skalować backend, stawiając kolejne instancje za load balancerem, bez lepkich sesji. To jedna z głównych przyczyn, dla których REST tak dobrze sprawdza się w architekturach chmurowych i kontenerach.
Kluczowa idea to orientacja na zasoby. Zamiast wywoływać procedury (jak w RPC), operujesz na zasobach identyfikowanych przez URL-e. /users/42 to zasób. GET /users/42 pobiera jego reprezentację. DELETE /users/42 go usuwa. URL-e opisują rzeczowniki, a metody HTTP są czasownikami.
Metody HTTP i idempotencja
REST opiera się na garstce metod HTTP, z których każda ma dobrze zdefiniowaną semantykę. Trzymanie się tych konwencji to najłatwiejszy sposób, żeby inni programiści od razu rozumieli twoje API.
Najważniejsze czasowniki
- GET — pobiera zasób. Bezpieczna i idempotentna, nie może modyfikować stanu serwera.
- POST — tworzy nowy zasób albo wykonuje operację, która nie pasuje do innych metod. Nie jest idempotentna.
- PUT — zastępuje zasób w całości pod wskazanym URL-em. Idempotentna.
- PATCH — modyfikuje część zasobu. Niekoniecznie idempotentna (zależy od implementacji).
- DELETE — usuwa zasób. Idempotentna.
Idempotencja znaczy tyle, że wielokrotne wywołanie tej samej operacji daje ten sam efekt co jedno wywołanie. Jeśli klient wyśle DELETE /users/42 trzy razy z rzędu, wynik jest identyczny jak po jednym żądaniu — użytkownik został usunięty. To właściwość kluczowa dla systemów, w których sieć bywa zawodna i klient musi bezpiecznie ponawiać żądania.
Pamiętaj, że POST nie jest idempotentny — dwukrotne wywołanie utworzy dwa zasoby. Dlatego w krytycznych operacjach (płatności, zamówienia) używa się idempotency keys: klient generuje unikalny identyfikator i przesyła go w nagłówku Idempotency-Key. Serwer zapamiętuje, że widział ten klucz, i przy powtórce zwraca oryginalną odpowiedź zamiast wykonywać operację drugi raz. Stripe rozpropagował tę praktykę i dziś jest de facto standardem dla API transakcyjnych.
# GET - pobranie listy użytkowników
curl https://api.example.com/users
# POST - utworzenie nowego użytkownika
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name":"Anna","email":"anna@example.com"}'
# PUT - pełna aktualizacja zasobu 42
curl -X PUT https://api.example.com/users/42 \
-H "Content-Type: application/json" \
-d '{"name":"Anna Nowak","email":"anna@example.com"}'
# DELETE - usunięcie zasobu
curl -X DELETE https://api.example.com/users/42
Kody statusu i struktura odpowiedzi
Kody HTTP to pierwsza linia komunikacji między serwerem a klientem. Używanie ich zgodnie z przeznaczeniem oszczędza mnóstwo pytań w dokumentacji.
Kody, które musisz znać
- 200 OK — żądanie zakończone sukcesem, ciało odpowiedzi zawiera dane.
- 201 Created — utworzono nowy zasób; warto zwrócić nagłówek
Locationz jego URL-em. - 204 No Content — sukces bez ciała odpowiedzi, typowe dla DELETE.
- 400 Bad Request — niepoprawne żądanie (np. zły JSON).
- 401 Unauthorized — brak lub niepoprawne uwierzytelnienie.
- 403 Forbidden — jesteś zalogowany, ale nie masz uprawnień.
- 404 Not Found — zasób nie istnieje.
- 409 Conflict — konflikt stanu, np. próba utworzenia duplikatu.
- 422 Unprocessable Entity — poprawne żądanie, ale dane nie przechodzą walidacji biznesowej.
- 500 Internal Server Error — błąd po stronie serwera, klient nie zawinił.
Rozróżnienie między 400 a 422 ratuje życie: 400 dla błędów składniowych (zły JSON, brakujący nagłówek), 422 dla błędów walidacji (email w złym formacie, wiek ujemny). Frontend może wtedy pokazać inną obsługę dla każdego przypadku.
Ciało odpowiedzi warto utrzymać w spójnym formacie. Najczęstszy wybór to JSON — lekki, czytelny i natywnie obsługiwany w JavaScripcie. XML wciąż spotkasz w starszych systemach (SOAP, rządowe API, niektóre enterprise’y), ale dla nowego API JSON jest domyślną odpowiedzią. Zwróć też uwagę na nagłówki: Content-Type: application/json; charset=utf-8 po stronie serwera i Accept: application/json po stronie klienta dają jednoznaczny kontrakt. Nagłówek Location po POST, który zwraca 201, wskazuje URL nowo utworzonego zasobu, a ETag i Last-Modified wspierają walidację cache i operacje warunkowe przez If-Match.
{
"data": {
"id": 42,
"name": "Anna Nowak",
"email": "anna@example.com",
"created_at": "2026-05-11T08:30:00Z"
},
"meta": {
"request_id": "req_7f3a9c"
}
}
Uwierzytelnianie i autoryzacja
API bez uwierzytelnienia to wyjątek, nie reguła. Do wyboru masz kilka sprawdzonych mechanizmów, a wybór zależy od kontekstu — publiczne API, wewnętrzne usługi, czy integracja z kontem użytkownika.
API keys, Bearer tokens i OAuth 2.0
Klucze API to najprostsze rozwiązanie — statyczny ciąg znaków identyfikujący aplikację. Dobre do mierzenia limitów i odcinania dostępu, ale nie nadają się do reprezentowania użytkownika końcowego.
Bearer tokens (zwykle JWT) przesyłasz w nagłówku Authorization. Są krótkotrwałe, podpisane kryptograficznie i mogą nieść informacje o tożsamości użytkownika oraz uprawnieniach.
OAuth 2.0 to framework do delegowanego dostępu — pozwala aplikacji działać w imieniu użytkownika bez poznania jego hasła. Flow authorization code jest standardem dla aplikacji webowych, client credentials dla komunikacji serwer-serwer, a PKCE to rozszerzenie dla aplikacji mobilnych i SPA. Warto pamiętać, że OAuth 2.0 sam w sobie nie jest protokołem uwierzytelniania — do tego służy nadbudowany nad nim OpenID Connect (OIDC), który dodaje id_token z informacjami o tożsamości.
Niezależnie od wybranego mechanizmu, nigdy nie przesyłaj tokenów w parametrach URL — trafiają do logów serwera, historii przeglądarki i loga proxy. Zawsze używaj nagłówka Authorization albo bezpiecznego, HttpOnly cookie. Tokeny długotrwałe (refresh tokens) trzymaj na serwerze, nie w localStorage SPA, bo są podatne na ataki XSS.
import requests
url = "https://api.example.com/users/me"
headers = {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9...",
"Accept": "application/json"
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
user = response.json()
print(f"Witaj, {user['name']}")
elif response.status_code == 401:
print("Token wygasł - odśwież go przez /auth/refresh")
else:
response.raise_for_status()
Paginacja, wersjonowanie i limity
Cursor vs offset
Kiedy lista zasobów rośnie, musisz ją podzielić. Dwa główne podejścia to offset-based (?page=3&limit=20) i cursor-based (?cursor=eyJpZCI6MTAwfQ&limit=20). Offset jest intuicyjny, ale na dużych zbiorach ma problemy z wydajnością i spójnością — gdy ktoś doda rekord między stronami, zobaczysz ten sam wiersz dwa razy. Cursor koduje pozycję w zbiorze (zwykle ID ostatniego rekordu) i jest stabilny nawet przy aktywnych zapisach. Duże API — GitHub, Stripe, Twitter — od dawna używają kursorów.
Wersjonowanie
Wcześniej czy później zmienisz kontrakt API w sposób niekompatybilny wstecz. Trzy najczęstsze strategie to: wersja w URL (/v1/users, /v2/users) — prosta i widoczna, ale zaśmieca ścieżki; wersja w nagłówku (Accept: application/vnd.example.v2+json) — czystsza, ale trudniejsza do testowania przez curl; wersjonowanie per endpoint — elastyczne, ale wymaga dyscypliny. Dla większości projektów wystarczy wersja w URL, zwłaszcza na początku.
Rate limiting
Każde publiczne API powinno mieć limity, inaczej jeden źle napisany klient zablokuje usługę wszystkim. Standardowe nagłówki, których używaj:
X-RateLimit-Limit— maksymalna liczba żądań w oknieX-RateLimit-Remaining— ile zostałoX-RateLimit-Reset— timestamp odświeżenia licznikaRetry-After— ile sekund czekać po otrzymaniu 429
Kod statusu 429 Too Many Requests sygnalizuje przekroczenie limitu. Klient powinien zaimplementować exponential backoff — czekać coraz dłużej przed ponowieniem.
HATEOAS, obsługa błędów i wybór między REST a GraphQL
Ostatnią cechą w modelu Richardsona jest HATEOAS (Hypermedia as the Engine of Application State) — odpowiedź zawiera linki do powiązanych akcji, dzięki czemu klient nie musi znać z góry struktury URL-i. W teorii świetny pomysł. W praktyce mało kto implementuje go w pełni, bo komplikuje klientów i dokumentację. Większość API pozostaje na poziomie 2 modelu Richardsona: zasoby + metody HTTP + kody statusu, bez hipermediów.
Jeśli chcesz dowiedzieć się więcej o naszym podejściu do budowania API i dokumentacji, zajrzyj do sekcji O nas. Pracujemy z frameworkami takimi jak FastAPI, Express i Django REST Framework — każdy z nich ma mocne strony i warto znać wszystkie trzy.
Ustandaryzowane błędy
Zamiast zwracać różne struktury błędów, trzymaj się jednego formatu. Dobry wybór to RFC 7807 (Problem Details) — standard IETF dokładnie dla tego przypadku.
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": "Pole email ma nieprawidłowy format.",
"instance": "/users",
"errors": [
{"field": "email", "code": "invalid_format"}
]
}
REST czy GraphQL?
GraphQL nie zastępuje REST — to inne narzędzie do innych problemów. Wybierz REST, gdy masz wyraźnie zasobowe API, chcesz wykorzystać HTTP cache, klienci są różnorodni (mobile, CLI, integracje zewnętrzne) i zależy ci na prostocie operacyjnej. Wybierz GraphQL, gdy klient musi pobrać dane z wielu źródeł naraz, struktura zapytań często się zmienia, albo masz jedną aplikację front-end, która dyktuje kształt odpowiedzi. Wiele firm utrzymuje oba — REST dla integracji zewnętrznych, GraphQL dla własnego frontendu.
Testowanie i następne kroki
Najprostszy sposób nauki REST to po prostu wysyłanie żądań. curl jest zawsze pod ręką, httpie daje przyjemniejszą składnię (http POST api.example.com/users name=Anna), a Postman i Insomnia pozwalają zapisywać kolekcje i testować scenariusze end-to-end. Dla testów automatycznych używaj pytest z biblioteką requests w Pythonie, supertest w Node.js lub RestAssured w Javie.
Kiedy zaczniesz projektować własne API, poświęć dzień na OpenAPI (dawniej Swagger). To specyfikacja, która opisuje twoje endpointy w formacie YAML lub JSON, a na jej podstawie narzędzia generują dokumentację, klientów i mocki. FastAPI generuje OpenAPI automatycznie z type hintów Pythona. Django REST Framework ma drf-spectacular. Express — swagger-jsdoc. To inwestycja, która zwraca się już po pierwszej większej integracji.
Dobrą praktyką przy testowaniu jest budowanie środowiska sandbox — oddzielnego egzemplarza API z fałszywymi danymi, którego klienci mogą używać bez ryzyka zepsucia produkcji. Stripe, PayPal i Twilio pokazują, jak to powinno wyglądać: identyczna powierzchnia API, zwracane realistyczne odpowiedzi, wyraźne oznaczenia w nagłówkach. Inwestycja w dobry sandbox zwraca się w jakości integracji zewnętrznych i mniejszej liczbie zgłoszeń do supportu.
Kiedy opanujesz klasyczne REST-owe wzorce, zajrzyj do tematów, które naturalnie idą w parze: webhooki jako odwrócony kierunek komunikacji (serwer woła klienta), streaming Server-Sent Events i WebSockets dla aktualizacji w czasie rzeczywistym, a na koniec — wzorce mikroserwisowe, gdzie twoje API staje się częścią większej sieci usług komunikujących się z sobą. Każdy z tych tematów rozwija REST-a w konkretną stronę i warto znać je, zanim trafisz na projekt, w którym samo REST przestaje wystarczać.

