Ale REST to nie tylko "używaj HTTP". To zbiór konwencji i zasad, które sprawiają, że API jest intuicyjne, przewidywalne i łatwe w użyciu.
W tym artykule poznasz zasady projektowania REST API to interfejs udostępniający dane przez standardowe metody HTTP (GET, POST...) pod adresami zasobów — w WordPressie domyślnie pod /wp-json/. — zarówno jako konsument (frontend), jak i twórca (backend). Szerszy kontekst aplikacyjny znajdziesz w przewodniku Backend dla frontendowca: serwer, bazy danych i API.
Zasady REST
REST opisuje sześć właściwości, które razem tworzą architekturę zwaną Representational State Transfer. Warto je znać nie po to, żeby cytatować ze specyfikacji, ale żeby rozumieć, dlaczego pewne decyzje projektowe są lepsze od innych.
1. Client-Server
Klient (frontend) i serwer (backend) są od siebie oddzielone i komunikują się przez HTTP. Ta separacja oznacza, że frontend może ewoluować niezależnie od backendu — wystarczy zachować kontrakt API. Możesz na przykład przepisać całe UI na inny framework bez dotykania backendu, albo wymienić silnik bazy danych bez wpływu na klientów.
2. Stateless
Każdy request powinien zawierać wszystko, czego serwer potrzebuje do jego obsługi — bez polegania na stanie sesji po stronie serwera. W praktyce wiele API łączy to z tokenami JWT w nagłówkach Authorization, co pozwala na stateless uwierzytelnianie. Jeśli API korzysta z sesji serwerowych lub cookies, jest to technicznie odejście od czystego REST, ale w realu to częsty kompromis — ważniejsze jest, żeby kontrakt był spójny i dobrze udokumentowany.
3. Cacheable
Odpowiedzi powinny jawnie informować klientów, czy można je cachować i jak długo. Nagłówki Cache-Control i ETag to podstawa — dzięki nim przeglądarka, CDN lub reverse proxy mogą serwować odpowiedzi bez angażowania backendu. Dla API publicznych to realny wpływ na koszty infrastruktury i czas odpowiedzi.
4. Uniform Interface
Jednolity interfejs to serce REST — zasoby identyfikowane przez URL, operacje przez metody HTTP, standaryzowane kody statusu. Dzięki tej jednolitości developer integrujący nowe API może w dużej mierze przewidzieć jego zachowanie bez czytania dokumentacji od deski do deski.
5. Layered System
Klient nie powinien zakładać, że komunikuje się bezpośrednio z serwerem docelowym. Pomiędzy nimi mogą stać load balancery, cache'e, API gateway czy reverse proxy — i to jest poprawna, wręcz pożądana architektura. Dobry design REST uwzględnia, że warstwa pośrednia może np. cachować GET-y albo rozdzielać ruch.
6. Code on Demand (opcjonalne)
Serwer może opcjonalnie przekazywać klientowi wykonywalny kod (np. JavaScript). W praktyce ta zasada jest rzadko stosowana i często pomijana w opisach REST.
Struktura URL
Zasoby (Resources)
Pierwsza zasada, której złamanie natychmiast widać w code review: URL powinien nazywać zasób, a nie akcję. Metoda HTTP jest od opisywania co robisz — URL jest od opisywania z czym.
Wzorzec z czasownikami pochodzi z epoki SOAP i RPC — w REST nie ma dla niego miejsca. Jeśli masz ochotę napisać /deleteUser/123, to sygnał, że powinieneś napisać DELETE /users/123.
Kolekcje vs pojedyncze zasoby
Ten podział jest fundamentem REST: kolekcja zasobów ma inny URL niż pojedynczy zasób, a operacje na obu wyglądają inaczej.
POST /users tworzy nowy zasób w kolekcji. DELETE /users/123 usuwa konkretny zasób z kolekcji. Frontend integrujący takie API może wywnioskować semantykę bez czytania dokumentacji.
Zagnieżdżone zasoby
Zagnieżdżenie w URL wyraża relację między zasobami — komentarze należą do posta, posty należą do użytkownika:
Zasada praktyczna: zatrzymaj się na maksymalnie 2-3 poziomach. Głębsze drzewo (/users/123/posts/456/comments/789/replies) jest trudne do czytania i sugeruje, że relacja powinna być modelowana inaczej — np. przez filtrowanie: GET /replies?commentId=789.
Konwencje nazewnictwa
Konsekwencja w nazewnictwie jest ważniejsza niż wybór konkretnej konwencji — ale warto wybrać tę, którą web de facto stosuje:
Liczba mnoga dla kolekcji odzwierciedla fakt, że zasób to zbiór rekordów, nie jeden rekord. Kebab-case jest standardem webowym — camelCase jest dla właściwości JSON, nie segmentów URL.
Metody HTTP i CRUD
Metoda HTTP wyraża intencję operacji — to ta część requestu, która odpowiada na pytanie "co chcesz zrobić z tym zasobem?". Mapowanie na CRUD to skrót od Create, Read, Update, Delete, czyli podstawowych operacji wykonywanych na danych. jest intuicyjne, ale ma kilka niuansów, które warto znać zanim zaczniesz projektować API.
| Metoda | CRUD | Opis | Idempotentna? | Przykład |
|---|---|---|---|---|
| GET | Read | Pobierz zasób | Tak | GET /users/123 |
| POST | Create | Utwórz zasób | Nie | POST /users |
| PUT | Update | Zastąp zasób | Tak | PUT /users/123 |
| PATCH | Update | Częściowa aktualizacja | Zależy | PATCH /users/123 |
| DELETE | Delete | Usuń zasób | Tak | DELETE /users/123 |
Idempotencja oznacza, że wielokrotne wywołanie tej samej operacji daje ten sam efekt. Jeśli klient wyśle DELETE /users/123 dwa razy — drugi raz powinien dostać 404, ale stan serwera jest ten sam. Przy POST każde wywołanie tworzy nowy zasób — retry bez zabezpieczenia to duplikat.
GET — pobieranie
POST — tworzenie
PUT — pełna aktualizacja
PUT zazwyczaj oznacza pełną reprezentację zasobu — jeśli nie przekażesz jakiegoś pola, serwer powinien je wyzerować lub usunąć. To jest często źródło bugów, gdy frontend wysyła tylko zmodyfikowane pola, a serwer traktuje brakujące jako "usuń". Dlatego PUT warto stosować tylko wtedy, gdy faktycznie chcesz zastąpić cały zasób.
PATCH — częściowa aktualizacja
PATCH aktualizuje tylko przekazane pola. To najwygodniejsza metoda dla typowych operacji — zmiana emaila, statusu, jednej właściwości — bez ryzyka przypadkowego wyzerowania reszty obiektu. Warto jednak pamiętać, że semantyka PATCH nie jest tak rygorystycznie ustandaryzowana jak PUT — różne API implementują ją różnie, dlatego dokładne zachowanie powinno być opisane w dokumentacji.
DELETE — usuwanie
Kody statusu HTTP
Kody statusu to pierwsza linia komunikacji między serwerem a klientem — zanim frontend przeczyta body odpowiedzi, sprawdza kod. Właściwe kody pozwalają frontendowi podjąć decyzję (pokaż błąd, przekieruj na login, odśwież token) bez parsowania treści. Błędne kody — np. 200 z błędem w body — łamią tę konwencję i utrudniają cachowanie, obsługę błędów i debugowanie.
2xx — Sukces
| Kod | Znaczenie | Kiedy |
|---|---|---|
| 200 | OK | GET, PUT, PATCH sukces |
| 201 | Created | POST sukces (nowy zasób) |
| 204 | No Content | DELETE sukces, brak body |
201 Created powinien pojawić się przy każdym POST, który tworzy zasób. Warto przy nim zwracać nagłówek Location z URL nowego zasobu — klient wie wtedy, gdzie sięgnąć po szczegóły bez dodatkowego GET-a. 204 No Content jest właściwą odpowiedzią na DELETE — operacja zakończyła się sukcesem, ale nie ma nic do zwrócenia.
4xx — Błąd klienta
| Kod | Znaczenie | Kiedy |
|---|---|---|
| 400 | Bad Request | Nieprawidłowe dane |
| 401 | Unauthorized | Brak/nieprawidłowy token |
| 403 | Forbidden | Brak uprawnień |
| 404 | Not Found | Zasób nie istnieje |
| 409 | Conflict | Konflikt (duplikat) |
| 422 | Unprocessable Entity | Błąd walidacji |
| 429 | Too Many Requests | Rate limit |
Dwa kody, które regularnie powodują zamieszanie:
401 vs 403 — 401 znaczy "nie wiem kim jesteś, zaloguj się", 403 znaczy "wiem kim jesteś, ale nie wolno Ci". Różnica jest fundamentalna dla frontend handlera: przy 401 powinno nastąpić przekierowanie na login lub odświeżenie tokenu, przy 403 wystarczy pokazać komunikat o braku uprawnień.
400 vs 422 — 400 to ogólny błąd formatu requestu (niepoprawny JSON, brakujące wymagane nagłówki), 422 to poprawny syntaktycznie request, który nie przeszedł walidacji biznesowej (np. email jest stringiem, ale nie ma znaku @). W praktyce wiele API używa tylko 400 dla obu przypadków — ważniejsze niż wybór kodu jest spójny, szczegółowy format błędu w body.
409 Conflict jest właściwy przy próbie stworzenia duplikatu (użytkownik z tym samym emailem już istnieje) lub przy konflikcie optymistycznego lockingu.
5xx — Błąd serwera
| Kod | Znaczenie | Kiedy |
|---|---|---|
| 500 | Internal Server Error | Nieoczekiwany błąd |
| 502 | Bad Gateway | Problem z upstream |
| 503 | Service Unavailable | Serwer przeciążony |
Błędy 5xx zawsze należą do serwera — klient nie może z nimi nic zrobić poza poczekaniem i ponowieniem. 503 Service Unavailable powinien być zwracany z nagłówkiem Retry-After, żeby klient wiedział, kiedy próbować ponownie. Nigdy nie zwracaj 5xx dla błędów walidacji czy braku uprawnień — to 4xx, i to ważne rozróżnienie przy monitoringu i alertach.
Format odpowiedzi
Dobry format odpowiedzi to taki, którego klient nie musi zgadywać. Jeśli sukces wygląda inaczej przy GET a inaczej przy POST, a błąd ma różną strukturę w zależności od endpointa — integracja staje się obroną przed własnym API.
Sukces
Daty powinny być zawsze w formacie ISO 8601 (2025-01-15T10:30:00Z) — łatwe do parsowania w każdym języku, jednoznaczne co do strefy czasowej.
Błąd — RFC 9457
Standard RFC 9457 (Problem Details for HTTP APIs) definiuje jednolity format dla błędów API. Zamiast wymyślać własną strukturę, warto trzymać się tego standardu:
Pola type (URI identyfikujące klasę błędu), title (czytelna dla człowieka nazwa klasy), status (kod HTTP) i detail (szczegółowy opis) tworzą maszynoczytelną i jednocześnie czytelną dla developera strukturę. instance wskazuje konkretne wystąpienie błędu, co ułatwia debugowanie i korelację z logami.
Nawet jeśli nie chcesz pełnego RFC 9457, trzymaj się jednej zasady: spójny format błędów przez całe API. Mieszanie { "error": "..." }, { "message": "..." } i { "errors": [...] } w różnych endpointach to klasyczny dług techniczny.
Kolekcja z metadanymi
Wrapper data + meta jest przydatny, bo pozwala rozszerzyć odpowiedź o metadane bez breaking change — wystarczy dodać nowe pole do meta.
Filtrowanie, sortowanie, paginacja
Filtrowanie (query parameters)
Sortowanie
Alternatywna konwencja:
Paginacja
Offset-based:
Cursor-based (lepsze dla dużych zbiorów):
Offset pagination jest prostsza do implementacji i wygodna przy panelach admina, gdzie użytkownik chce przejść na konkretną stronę wyników. Ma jednak wadę przy dużych, zmieniających się zbiorach: jeśli ktoś doda rekord między requestami, page 2 może zawierać duplikaty z page 1 albo pominąć rekord.
Cursor pagination rozwiązuje ten problem — każda strona zaczyna od konkretnego punktu w zbiorze (zwykle ID lub timestamp ostatniego rekordu), niezależnie od tego, co się zdarzyło w zbiorze między requestami. To właściwy wybór dla nieskończonego przewijania, real-time feedów i API z wysoką częstotliwością zmian danych.
Odpowiedź z paginacją:
Wybór pól (sparse fieldsets)
To wygodne, ale warto whitelistować pola po stronie serwera, żeby klient nie mógł wyciągać wszystkiego bez kontroli.
Include (eager loading)
Tak samo jak przy fields, warto posiadać limit dopuszczalnych relacji, żeby nie zrobić przypadkiem własnego "mini-GraphQL bez guardrailów".
Wersjonowanie API
W URL (najpopularniejsze)
W nagłówku
W query parameter
Rekomendacja: Wersjonowanie w URL jest najprostsze i najbardziej widoczne, ale nie każde API musi zaczynać od /v1, dlatego czasami wystarczy po prostu dobra kompatybilność wstecz i przemyślane polityki zmian.
Autoryzacja
Bearer Token (JWT)
API Key
Odpowiedzi
Wersjonowanie API — kiedy i jak
Wersjonowanie API to jeden z tych tematów, które boli dopiero wtedy, gdy jest za późno. Jeśli masz publiczne API i wprowadzisz breaking change bez wersjonowania, psujesz wszystkich konsumentów naraz.
Breaking change to m.in.:
- zmiana nazwy pola lub usunięcie pola z odpowiedzi
- zmiana typu pola (string → number)
- zmiana znaczenia kodu statusu dla danego endpointa
- usunięcie endpointa
Non-breaking change to m.in.:
- dodanie nowego opcjonalnego pola w odpowiedzi
- dodanie nowego endpointa
- dodanie opcjonalnego query parametru
Wersjonowanie w URL (/api/v1/users) jest najpopularniejsze i najprostsze — widać je w URL, łatwo testować, logować i routować na różne implementacje. Wadą jest to, że trzeba utrzymywać kilka wersji jednocześnie. Praktyczna zasada: zacznij od /v1 przy pierwszym publicznym release'ie, nawet jeśli nie masz jeszcze /v2 w planach. Dodanie wersjonowania do bezwersyjnego API jest samo w sobie breaking change.
Rate Limiting
API powinno informować o limitach:
Po przekroczeniu:
Idempotency dla wrażliwych POST-ów
Tworzenie zasobów po POST nie jest idempotentne, przy płatnościach, webhookach albo retriable requestach warto dodać klucz idempotencyjny:
Serwer powinien rozpoznać ponowienie tego samego żądania i nie wykonać operacji drugi raz.
HATEOAS (opcjonalne)
Hypermedia As The Engine Of Application State, czyli linki do powiązanych zasobów:
W praktyce jest to rzadko w pełni implementowane, ale warto chociaż poznać koncept.
Przykład: API do bloga
Endpoints
Przykładowe żądania
Chcesz zbudować własne API? Przejdź do Backend dla frontendowca: serwer, bazy danych i API.
