1. Problem: API komendowe zamienia się w chaos
Niemal każdy początkujący backend-deweloper prędzej czy później projektuje API „jak pilot od telewizora": na każdą akcję — osobny przycisk. Na początku nawet to fajne. Jest akcja „utwórz zadanie" — będzie /createTask. Jest akcja „zakończ zadanie" — będzie /completeTask. Wydaje się logiczne: dosłownie przenosisz swoje myśli do URL. Ale gdy system zaczyna żyć, przycisków robi się 50, potem 150, i w pewnym momencie sam już nie pamiętasz, gdzie masz „zakończ", gdzie „archiwizuj" i czym jedno różni się od drugiego.
Wyobraź sobie, że do Task Tracker trzeba dodać trochę więcej funkcjonalności. Użytkownik chce przypisać realizatora, cofnąć przypisanie, zmienić priorytet, ustawić termin, usunąć termin, dodać komentarz, usunąć komentarz, dołączyć plik, usunąć plik, ponownie otworzyć zadanie… Jeśli to projektujesz komendowo, bardzo szybko powstaje cała kolekcja URI-zaklęć, gdzie klient musi nie rozumieć domeny, lecz zapamiętać słownik twoich czasowników.
I tu ujawnia się główny problem: klient nie potrzebuje listy twoich wewnętrznych „komend". Potrzebuje zrozumiałego zestawu obiektów i operacji, które wyglądają przewidywalnie i zachowują się przewidywalnie.
HTTP już dał podstawową gramatykę: method, path, headers, body, status. Ale sama gramatyka nie wystarczy, jeśli powierzchnia API jest jak katalog komend. Teraz ważne jest zrozumienie, wokół czego w ogóle układać domenę, żeby GET, POST i pozostałe metody działały na zrozumiałej mapie, a nie na zestawie przycisków.
REST w tym kursie: nie religia, lecz sposób na dogadanie się z klientem
Gdy ludzie słyszą „REST", często zaczyna się albo religijna wojna, albo lekka panika. „A co jeśli nie robimy prawdziwego REST?" „A HATEOAS jest potrzebny?" „A można czasowniki w URL?" Powiedzmy wprost: w komercyjnym developmencie REST — to nie egzamin z filozofii. To praktyczny sposób na zrobienie API czytelnym, przewidywalnym i omawialnym z zespołem i klientami.
W ramach tego kursu REST — to przede wszystkim zewnętrzny kontrakt. Czyli to, co widzi konsument twojego API: aplikacja mobilna, frontend, integracja partnera, Postman, autotesty, a czasem nawet inny backend. REST pomaga przestać myśleć: „jaką metodę kontrolera teraz napiszę?" — i zacząć myśleć: „jaki obiekt naszej dziedziny istnieje dla klienta?"
Usuńmy jeszcze jedno nieporozumienie. REST nie dyktuje wewnętrznej architektury. Wewnątrz możesz mieć choćby 30 serwisów, albo jeden ogromny (tak robić nie należy, ale może być), albo warstwy controller -> service -> repository. Klientowi to obojętne. REST — o tym, jak wygląda powierzchnia systemu z zewnątrz, a nie o tym, jak zorganizowana jest kuchnia wewnątrz.
2. Zewnętrzny kontrakt vs nazwy metod
Żeby przestawić myślenie, pomocna jest prosta analogia: API — to menu w restauracji. Klient widzi menu. Nie wie, jak kucharz nazwał zmienną sauce2 i jaką metodę wywołuje w kuchni. Jeśli jutro przemianujecie cookCarbonara() na makePastaLikeInRome() — gościowi restauracji to obojętne. Ale jeśli nagle usuniecie z menu „Carbonarę" albo przemianujecie ją na „Makaron_42", gościowi będzie przykro i pójdzie do konkurencji (albo jeśli to integracja korporacyjna, przyśle do supportu bardzo grzeczny mail, z którego i tak będzie słychać krzyk).
Tak samo z API. Wewnątrz kodu faktycznie możesz mieć komendy. Możesz mieć serwis z metodami completeTask(taskId) albo addComment(taskId, text). To normalny wewnętrzny model zachowania. Problem zaczyna się, gdy decydujesz: „Skoro mam metodę completeTask, to i URL będzie /completeTask". Czyli zewnętrzny kontrakt staje się lustrem wewnętrznych nazw metod.
Mała ilustracja w Javie. Wewnętrzne komendy aplikacji mogą wyglądać tak:
class TaskCommands {
// Model wewnętrzny: "komendy" aplikacji (jak nazywasz metody u siebie w kodzie)
void createTask() {}
// taskId — wewnętrzny identyfikator zadania, klientowi ważne, że to "zadanie", a nie "komenda"
void completeTask(String taskId) {}
}
A zewnętrzny kontrakt — to, jakie dane pokazujesz i jak do nich się odwołujesz — wygodniej postrzegać tak: „oto zadanie, oto jego reprezentacja dla klienta":
// Model zewnętrzny: DTO/reprezentacja zasobu "zadanie" dla klienta
record TaskView(String id, String title, String status) {}
Sens nie w tym, że record magicznie zamienia API w REST. Sens w tym, że zewnętrzny świat powinien mówić językiem zadań, a nie językiem wewnętrznych komend.
3. Przejście do dziedziny klienta
Jeśli z tej lektury zapamiętasz tylko jedno zdanie, niech to będzie: w myśleniu REST główne pytanie brzmi: „Z czym dokładnie pracuje klient?" Nie „co robimy", nie „jaką metodę wywołujemy", lecz „jaki obiekt dziedziny jest tu główny".
Dla Task Tracker API odpowiedź jest oczywista: główny obiekt — to zadanie. Właśnie wokół niego buduje się sens systemu. Komentarze istnieją przy zadaniu. Załączniki też. Tagi w naszym projekcie edukacyjnym — to raczej wygodne wartości do wyszukiwania i klasyfikowania zadań, a nie osobna duża encja żyjąca własnym życiem. Status — to część stanu zadania, a nie samodzielny „obiekt komendy".
Miły efekt uboczny tego pytania: zmusza do myślenia nie „gdzie wcisnąć endpoint", lecz „która część domeny jest teraz omawiana". A co za tym idzie, API zaczyna przypominać mapę terenu, a nie listę przypadkowych zaklęć.
Oto krótki przykład, który pokazuje różnicę w myśleniu, bez wchodzenia w Spring i adnotacje:
import java.util.List;
// Styl "komendowy": endpoint == czasownik/operacja, którą trzeba zapamiętać
List<String> rpcLike = List.of(
"/createTask",
"/completeTask",
"/getAllTasks"
);
// Styl "zasobowy": endpoint == encja/zasób, z którym pracuje klient
List<String> resourceLike = List.of(
"/tasks",
"/tasks/{taskId}"
);
Pierwszy zestaw brzmi jak zestaw przycisków na pilocie. Drugi — jak „mamy zadania; czasem pracujemy z listą, czasem — z konkretnym zadaniem". I to „czasem z listą, czasem — z konkretnym" to już pierwszy krok do stabilnego kontraktu.
4. Styl RPC-like: wygodny, ale żyje źle
API RPC-like — to gdy zewnętrzny interfejs systemu wygląda jak „zdalne wywołanie metod". URL zamienia się w nazwę komendy, a żądanie HTTP znaczeniowo sprowadza się do „wywołaj mi tę funkcję na serwerze". Z punktu widzenia początkującego faktycznie wydaje się to wygodne: piszesz metodę i prawie tymi samymi słowami nazywasz endpoint. Minimum przemyśleń, maksimum szybkości. Niemal jak kopiuj-wklej, tylko w głowie.
Ale takie podejście ma nieprzyjemną cechę: źle się skaluje. Każdy nowy scenariusz dodaje nową czasownikową trasę. Do tego miesza dwie rzeczy, które HTTP specjalnie rozdziela: adres (path) i akcję (method). W efekcie łatwo dochodzi się do czegoś w stylu GET /deleteTask — czyli „czytaniem" wyzwala się „usuwanie". HTTP się smuci, caching się myli, idempotency zaczyna brzmieć jak przekleństwo, a klient musi zapamiętywać wyjątki.
Porównajmy na poziomie sensu (nie implementacji). Oto jak wygląda forma „komendowa":
import java.util.Map;
// Klucz — "co chcemy zrobić", wartość — "jak wywołać komendę przez HTTP"
Map<String, String> commands = Map.of(
"complete", "POST /completeTask",
"delete", "GET /deleteTask/{taskId}"
);
A oto jak zaczyna wyglądać forma „zasobowa", gdzie główny obiekt — to zadanie:
import java.util.Map;
// Klucz — "jaki zasób", wartość — "jak z nim pracuje metoda HTTP + ścieżka"
Map<String, String> resources = Map.of(
"task collection", "POST /tasks",
"one task", "DELETE /tasks/{taskId}"
);
Zauważ: teraz w ogóle nie jest ważne, jaka dokładnie metoda HTTP okaże się prawidłowa w każdym scenariuszu — to będziemy omawiać później w kursie. Ważne jest co innego: w drugim wariancie ta sama encja dziedziny czyta się jednakowo. I dlatego API jest znacznie łatwiejsze do omówienia. Nie dyskutujesz „jaki mamy endpoint do zakończenia?", lecz omawiasz: „co oznacza zakończenie zadania jako zmiana jego stanu?".
5. Podobne operacje powinny wyglądać podobnie
REST jest wartościowy nie tym, że robi URL „ładnymi", lecz tym, że czyni API rozpoznawalnym. Rozpoznawalność — to gdy klient, widząc po raz pierwszy fragment API, z dużym prawdopodobieństwem może odgadnąć resztę. Nie dlatego, że jest telepatą, lecz dlatego, że projektujesz kontrakt systemowo: podobne — wygląda podobnie.
Przewidywalność szczególnie ratuje, gdy API rośnie. Pojawia się więcej zasobów, więcej scenariuszy, więcej deweloperów w zespole. I tu straszna prawda: nikt nie utrzyma w głowie stu „unikalnych" komendowych endpointów. Natomiast zestaw zasobów i czytelne reguły „jak się z nimi zwykle pracuje" — można.
Można to postrzegać jak język. Jeśli w języku nie ma gramatyki, pozostaje zakuwanie. „Ta komenda nazywa się tak, a ta — tak, a ta — wyjątek". Jeśli gramatyka jest, możesz budować nowe zdania bez uczenia się każdego z osobna. REST — to taka „gramatyka" dla API na bazie HTTP: dziedzinę adresujemy przez URI, a charakter operacji wyrażamy metodami i zrozumiałymi umowami.
Żeby to poczuć inżyniersko, wystarczy prosta mini-weryfikacja: jeśli widzę /tasks/{taskId}, czy mogę przynajmniej mniej więcej oczekiwać, że operacje wokół zadania będą jakoś powiązane z tą samą ścieżką, a nie rozlecą się po /doSomethingWithTask? W stylu zasobowym odpowiedź zazwyczaj brzmi „tak". W komendowym — „nie wiem, trzeba poszukać w dokumentacji… jeśli w ogóle istnieje".
6. Mini-case na Task Tracker API
Teraz nie projektujemy finalnych URI i nie zatwierdzamy wersjonowania /api/v1 (to będzie później). Ale już teraz można zobaczyć, jak ta sama domena albo staje się czytelna, albo zamienia się w bałagan — w zależności od wybranego stylu.
Wyobraź sobie, że chcesz pokryć podstawowe scenariusze Task Tracker (utwórz zadanie, pokaż listę, pokaż jedno, dodaj komentarz, załaduj załącznik, pobierz listę tagów). Styl komendowy bardzo łatwo prowadzi do czegoś takiego:
import java.util.List;
// Słaba skalowalność: nowe funkcje = nowe "czasownikowe" endpointy
List<String> rpcStyle = List.of(
"/createTask",
"/getAllTasks",
"/getTaskById",
"/addCommentToTask",
"/uploadFileToTask",
"/getAllTags"
);
Na starcie wydaje się: „no dobra". Ale spróbuj kontynuować tę listę jeszcze przez tydzień developmentu. Bardzo szybko następuje eksplozja czasownikowych tras: na każdy drobiazg — osobny endpoint, często jeszcze z czasownikami w różnych formach (create, add, upload, getAll, find, fetch…), i nikt już nie gwarantuje spójności.
Styl zasobowy natomiast zmusza do zgrupowania wszystkiego wokół głównego obiektu:
import java.util.List;
// "Mapa" dziedziny: są zadania i pod-zasoby zadania, plus osobne słowniki
List<String> resourceStyle = List.of(
"/tasks",
"/tasks/{taskId}",
"/tasks/{taskId}/comments",
"/tasks/{taskId}/attachments",
"/tags"
);
I tu w głowie pojawia się „mapa": są zadania, przy zadaniu są komentarze i załączniki, a tagi — to osobna mała lista wartości. Nawet jeśli jeszcze nie wiesz, jakie metody i statusy będą przy każdym endpoincie, powierzchnia systemu stała się już czytelna.
Zauważ jeszcze jeden ważny moment. W drugim wariancie niemal automatycznie zaczynasz rozmawiać o granicach. Na przykład komentarze żyją „pod zadaniem" — więc logicznie adresuje się je jako część zadania. A tagi w naszym projekcie edukacyjnym nie stają się osobnym dużym zasobem CRUD — więc żyją jako osobny endpoint lookup. Te decyzje nie są o „dobrze/źle" w próżni. Są o tym, żeby projekt edukacyjny pozostawał kompaktowy, zrozumiały i ewoluował bez zbędnych komplikacji.
7. Typowe błędy przy projektowaniu REST API
Błąd №1: mylić REST z „ładnymi path".
Bardzo łatwo sprowadzić REST do kosmetyki: „zróbmy po prostu URL w liczbie mnogiej — i będzie REST". Problem w tym, że liczba mnoga sama w sobie nie zamienia zestawu przypadkowych akcji w kontrakt. Żeby nie wpaść w tę pułapkę, warto za każdym razem wracać do pytania „jaki obiekt dziedziny pokazujemy klientowi?". Jeśli nie ma odpowiedzi, znaczy, że na razie wymyślasz nie zasób, lecz komendę.
Błąd №2: myśleć, że REST dyktuje wewnętrzną architekturę aplikacji.
Czasem zaczynają spierać się: „jeśli REST, to serwisy muszą być takie-a-takie", „więc nie można mieć metody completeTask()". Można. Wewnątrz kodu możesz mieć dowolne komendy, metody i nawet czasowniki w nazwach — to normalne. REST dyscyplinuje zewnętrzny kontrakt. Wnętrzności będziesz zmieniać dziesiątki razy, i właśnie dlatego warstwa zewnętrzna powinna być stabilna i niezależna od twoich wewnętrznych przemianowań.
Błąd №3: kopiować do zewnętrznego API nazwy wewnętrznych metod.
To najczęstsze „przejście z rozpędu": jest metoda uploadFileToTask() — więc będzie /uploadFileToTask. W efekcie zewnętrzny kontrakt zaczyna odzwierciedlać twoją bieżącą implementację, a nie domenę. Lekarstwo: wyobraź sobie, że klient nie zna twojego kodu i nigdy go nie zobaczy. Klient rozumie tylko domenę („zadania, komentarze, załączniki"). Wtedy URL z komendami przestaje wyglądać naturalnie.
Błąd №4: postrzegać REST jako dogmat lub egzamin na „czystość".
W prawdziwym życiu REST — to narzędzie jasności, a nie zawody o to, „kto bliżej RFC". Jeśli starać się idealnie spełniać wszystkie teoretyczne ograniczenia, można utonąć w szczegółach i zatrzymać development. Z drugiej strony, jeśli powiedzieć „REST — to jak chcę", wyjdzie chaos. Pragmatyczne podejście w tym kursie: utrzymujemy model zasobowy czytelnym i spójnym, ale nie zamieniamy projektu w akademicką dyskusję.
Błąd №5: gubić centralny obiekt systemu.
W Task Tracker API centralny obiekt — to zadanie. Jeśli projektujesz API tak, że „głównego obiektu" nie widać (wszystko jest rozmazane po komendach, a encja task pojawia się tylko jako parametr gdzieś w głębi), tracisz mapę dziedziny. Wtedy każde nowe funkcje dodawane są nie jako rozszerzenie systemu, lecz jako nowe chaotyczne przyciski. Dobra samo-weryfikacja: czy można w 10 sekund wyjaśnić po liście endpointów, „o czym jest ten serwis"? Jeśli nie można, najprawdopodobniej znowu wpadłeś w styl komendowy.
GO TO FULL VERSION