CodeGym /Kursy /Spring REST & MVC /URI jako część kontraktu

URI jako część kontraktu

Spring REST & MVC
Poziom 4, Lekcja 0
Dostępny

1. Ścieżka jako część kontraktu

Ścieżka w API — to część publicznego kontraktu, a nie mały techniczny szczegół. Jeśli wcześniej pisałeś kontrolery „dla siebie", łatwo przyzwyczaić się do myślenia, że można ją zmienić w każdej chwili. W prawdziwym API ścieżka — to publiczny adres, jak adres dostawy. Możesz przestawiać meble w mieszkaniu (refaktoryzacja kodu), ale jeśli nagle zmienisz adres domu (URI), kurier (klient) przyjedzie w pole, będzie się smucił i dzwonił do supportu.

URI (ścieżka) — to część kontraktu, którą klient zapamiętuje. Nawet jeśli klient — to znowu ty, tylko w roli frontendowca „ty z przyszłości", który za miesiąc otworzy kod i powie: „a dlaczego tu jest /api/v1/getTasks, a tu /api/v1/tasks/list… kto to zrobił?". API nie ma mimiki ani intonacji: nie może „wyjaśnić" klientowi, co miałeś na myśli. Klient czyta kontrakt dosłownie: zobaczył URI — sformułował żądanie — oczekuje przewidywalnego zachowania.

I tu ważna myśl dla całego kursu: dobry URI przeżywa zmiany wewnętrznej implementacji. Możesz zmieniać nazwy klas, przenosić serwisy między pakietami, zastępować repozytorium in-memory inną warstwą przechowywania, ale /api/v1/tasks powinno pozostawać /api/v1/tasks, jeśli sens zasobu się nie zmienił. Dlatego URI projektuje się „z zewnątrz do wewnątrz": z pozycji konsumenta API, a nie według zasady „jak nazywa się metoda w kontrolerze".

2. Anatomia URI: segmenty i szablony

Zanim spieramy się, czy pisać „tasks" czy „task-items", warto umówić się co do podstawowych terminów. URI w HTTP składa się zazwyczaj ze ścieżki i ewentualnie query string, na przykład /api/v1/tasks?status=TODO. Dziś skupiamy się właśnie na ścieżkach: to ta część adresu, podzielona segmentami przez /, która pokazuje, do jakiego zasobu się zwracamy. Parametry query na razie zostawiamy na boku: dla rozmowy o ścieżce ważne jest drzewo adresowe zasobu, a nie konfiguracja żądania jak status=TODO.

Najprościej myśleć o ścieżce jako o łańcuchu segmentów, i każdy z nich powinien czytać się jak słowo w małym zdaniu. Jeśli segmenty — to przypadkowy zestaw skrótów i czasowników, klient musi zgadywać. Jeśli to rzeczowniki i stabilne identyfikatory, API czyta się niemal jak mapa.

Mała ściągawka terminów (bez zanudzania, ale z pożytkiem):

Termin Co to jest Przykład
URI „Adres" zasobu (w kontekście HTTP API częściej mówimy o path + query) /api/v1/tasks/123
Segment ścieżki Część URI między / api, v1, tasks, 123
URI kolekcji Ścieżka do zestawu jednorodnych zasobów /api/v1/tasks
URI zasobu Ścieżka do jednego zasobu po id /api/v1/tasks/{taskId}
Szablon URI Zapis ścieżki z placeholderem (wygodne w dokumentacji i kodzie) /api/v1/tasks/{taskId}

Żeby wizualnie rozłożyć ścieżkę na segmenty, czasem pomaga bardzo prosty schemat. Tak, schemat w lekturze o URI — to już lekki inżynierski dramat, ale lepiej tak niż potem szukać bugów w nieistniejącym endpoincie:

flowchart LR
    A["/api/v1/tasks/7b9b2d5a-2a32-4b79-9a9c-2a9c2d0b9d1f"]
    A --> B["api"]
    B --> C["v1"]
    C --> D["tasks"]
    D --> E["7b9b... (taskId)"]

Zwróć uwagę na ważny niuans: w prawdziwym żądaniu nigdy nie wysyłasz nawiasów klamrowych. {taskId} — to tylko wygodny „mockup" do dokumentacji i opisu tras w kodzie. W prawdziwym żądaniu będzie tam konkretna wartość, na przykład UUID.

3. Kolekcja i pojedynczy zasób

Większość API buduje się z dwóch cegiełek: „kolekcja" i „jeden zasób". Gdy początkujący deweloper słyszy „zaprojektuj API", często rysuje w głowie dziesiątki ścieżek. W praktyce, gdy starannie wybierze URI kolekcji, URI pojedynczego zasobu wychodzi niemal automatycznie: wystarczy dodać identyfikator. I to świetna wiadomość — spójność można tu uzyskać niemal za darmo.

W Task Tracker API podstawowy przykład — to zadania. Kolekcja zadań — to lista, z którą klient pracuje jako z zestawem elementów. Jeden zasób zadania — to konkretne zadanie po id. Już na tym poziomie można uczynić API przewidywalnym: klient, widząc /tasks, niemal bez podpowiedzi oczekuje, że /tasks/{taskId} da jedno konkretne zadanie.

Dla utrwalenia — mała tabelka. Używamy pełnego prefiksu /api/v1, ale teraz ważna nie jest logika wersji, lecz sama logika adresu: ścieżka zaczyna się od nazwy zasobu.

Co adresujemy URI Jak to czyta człowiek
Kolekcja zadań /api/v1/tasks „Wszystkie zadania" (dokładniej: zestaw zadań)
Jedno zadanie /api/v1/tasks/{taskId} „Zadanie z identyfikatorem taskId"

To rozróżnienie na razie wystarczy: kolekcja żyje pod /api/v1/tasks, a konkretny zasób — pod /api/v1/tasks/{taskId}. Na poziomie kodu takie stringi też lepiej trzymać centralnie, ale sama formuła jest prosta: najpierw nazwa kolekcji, potem identyfikator zasobu.

4. Nazwy kolekcji: liczba mnoga

Kolekcje lepiej nazywać przewidywalnie niż oryginalnie. Na tym kroku łatwo wpaść w kreatywność: nazwać zadania jakoś niestandardowo, na przykład /issues, /todoItems, /mySuperTasks. I kreatywność — to piękne… dokładnie do momentu, gdy przypominasz sobie, że API ma konsumenta. Konsumentowi nie potrzebna jest twoja inspiracja — potrzebna jest przewidywalność. Dlatego w projektowaniu API zwykle wybieramy nudne, ale stabilne: zrozumiałe rzeczowniki i jednolity styl nazewnictwa.

Praktyczna zasada dla kolekcji w REST API: kolekcje nazywamy w liczbie mnogiej. To nie prawo fizyki, lecz konwencja obniżająca obciążenie poznawcze. /tasks jest postrzegane jako lista. /task jest postrzegane jako „jedno zadanie", a potem zaczyna się zamieszanie: „dlaczego /task zwraca tablicę?". Żadnej tragedii nie będzie, ale API stanie się mniej czytelne — to jak mała pęknięcie w fundamencie: dziś jej nie widać, a za miesiąc mieszkasz w domu pełnym pęknięć.

W naszym projekcie słownik zasobów jest już nakreślony przez domenę: tasks, comments, attachments, tags. Ważne tu nie tylko „jak nazwać", ale i trzymać jedną nazwę wszędzie. Jeśli dziś nazywasz zasób tasks, jutro nie trzeba wprowadzać obok /todo „bo tak krócej". Krótko będzie przez pierwsze 10 minut, a potem dostaniesz dwa równoległe światy robiące to samo.

Dobre i złe łatwiej pokazać wprost:

Sens Źle Dobrze Dlaczego
Kolekcja zadań /api/v1/task /api/v1/tasks Liczba mnoga od razu sugeruje listę
„Pobierz zadania" /api/v1/getTasks /api/v1/tasks Akcję wyraża metoda HTTP, nie ścieżka
„Lista zadań" /api/v1/taskList /api/v1/tasks Nie trzeba powielać „list" — kolekcja jest już listą

Osobny niuans, o którym często się zapomina: URI — to nie miejsce na wewnętrzne terminy z twojej bazy kodu. Klientowi obojętne, że w Javie klasa nazywa się TaskAggregateRoot albo TaskEntity. W URI mówimy językiem dziedziny, ale w „ludzkim" wydaniu. Jeśli dziedziną są zadania, zasób nazywa się tasks, a nie task-aggregates.

5. Identyfikator w ścieżce: {taskId}

Identyfikator w ścieżce wyznacza adres konkretnego zasobu. Kwestia id wydaje się prosta: no id i id, co tu omawiać. Ale właśnie tu wiele API zaczyna się plątać. URI zasobu — to adres konkretnej encji, a więc identyfikator musi żyć w ścieżce. Jeśli adresujesz jedno zadanie, logiczne jest przejście pod adres /tasks/{taskId}. To jak mieszkanie w bloku: blok — to „kolekcja mieszkań", a numer mieszkania — konkretny zasób wewnątrz.

W Task Tracker API identyfikatory zasobów mamy zdefiniowane jako stringi UUID. To wygodne zarówno edukacyjnie, jak i wystarczająco realistyczne: UUID nie ujawnia sensu biznesowego, nie da się go „odgadnąć przez przeszukiwanie", i pozostaje stabilny nawet gdy zmieni się sposób przechowywania danych. Na poziomie URI ważne jest pamiętanie: identyfikator powinien być nieprzezroczysty dla klienta i stabilny. Klient nie musi wiedzieć, że „42" — to „czterdziesty drugi rekord w bazie".

Jeszcze jedna przydatna umowa: w szablonach ścieżek używamy mówiących nazw placeholderów. Tak, można napisać {id}, ale gdy pojawi się zagnieżdżenie (a w naszym projekcie będą taskId, commentId, attachmentId), sam sobie podziękujesz za jawne nazwy. Są trochę dłuższe, za to znika pytanie „a to id czego dokładnie?".

Porównaj „szablon" i „prawdziwy URI":

- Szablon w dokumentacji lub opisie trasy: /api/v1/tasks/{taskId}
- Prawdziwe żądanie: /api/v1/tasks/7b9b2d5a-2a32-4b79-9a9c-2a9c2d0b9d1f

Ważne, żeby dosłownie zobaczyć formę adresu: /api/v1/tasks/{taskId} — to szablon, a /api/v1/tasks/7b9b... — prawdziwy URI. Teraz nie interesuje nas typ taskId, lecz to, że adres jednego zasobu powstaje z adresu kolekcji plus identyfikator.

6. Czasowniki w ścieżce i metody HTTP

Czasowniki w ścieżce są niemal zawsze zbędne: w HTTP do akcji jest już metoda żądania. Jeśli kiedyś miałeś doświadczenie z „API jako zestawem funkcji", ręka sama ciągnie się do ścieżek w stylu /getTasks, /createTask, /deleteTask. Wygląda to logicznie, dopóki nie przypomnisz sobie: w HTTP już jest czasownik — to metoda żądania. Gdy piszesz czasownik jeszcze w ścieżce, dostajesz podwójne kodowanie sensu. A podwójne kodowanie — to najlepszy przyjaciel sprzeczności: dziś masz GET /createTask, jutro POST /getTasks, i oto żyjesz w świecie, gdzie URI i metoda są ze sobą w konflikcie.

W podejściu REST ścieżka odpowiada na pytanie „jaki zasób?", a metoda HTTP — na pytanie „co zrobić?". Jeśli potrzebne są zadania, idziemy pod /tasks. Jeśli chcemy stworzyć zadanie — i tak pracujemy z zasobem zadań, tylko metoda będzie inna. To jeden z powodów, dla których REST API czyta się jak system, a nie jak „katalog komend".

Bardzo krótka ilustracja, niemal memiczna:

// Przykład: ta sama akcja ("pobierz listę") nie powinna być kodowana czasownikiem w URI.
String bad = "/api/v1/getTasks";
String good = "/api/v1/tasks";

// Sens operacji wyraża metoda HTTP, a nie czasownik w ścieżce.

I jeśli zapisać to jako stringi żądań (tylko jako ilustracja kontraktu, bez szczegółów odpowiedzi), różnica staje się jeszcze wyraźniejsza:

  • Źle: GET /api/v1/getTasks
  • Dobrze: GET /api/v1/tasks

Zły wariant mówi „co zrobić", ale nie pokazuje modelu zasobowego. Dobry wariant pokazuje model zasobowy, a akcja jest już odczytywana z metody HTTP. To jest właśnie test dojrzałości: jeśli widzisz /api/v1/getTasks, masz niemal na pewno myślenie w stylu RPC. Nic strasznego — wszyscy tak mają na początku, dziś po prostu świadomie to leczymy.

7. Stabilność URI i jednolity styl

Nie wystarczy zrobić ścieżkę po prostu działającą — ważne jest uczynienie jej trwałą. Ścieżkę łatwo zrobić „działającą", ale znacznie trudniej taką, żeby za pół roku wyglądała równie logicznie jak w dniu pisania. Stabilność URI — to nie „nigdy niczego nie zmieniaj", lecz żeby zmiany były rzadkie, przemyślane i uzasadnione. Jeśli zmieniasz URI dlatego, że „zmieniłem nazwę metody w kontrolerze" — to niemal zawsze zły powód.

Jednolitość zaczyna się od małych decyzji. Na przykład: segmenty piszemy małymi literami (tasks, a nie Tasks), staramy się trzymać segmenty krótkie, nie używamy podkreśleń (task_items) bez konieczności, nie dodajemy rozszerzeń plików (tasks.json), nie mnożymy synonimów. API — to jak słownik: jeśli ten sam sens jest nazywany różnymi słowami, czytelnik (klient) nie rozumie, czy to „to samo" czy „coś innego".

Warto nawet zafixować małe „zasady stylu" (brzmi nudno, ale oszczędza godziny życia):

Wybór Zalecenie na kurs Dlaczego pomaga
Wielkość liter lowercase Wizualnie jednolite, mniej niespodzianek
Nazwy zasobów rzeczowniki Ścieżka opisuje „co", nie „jak"
Kolekcje liczba mnoga Szybko czyta się jako lista
Slash na końcu bez kończącego slasha Mniej duplikatów w stylu /tasks i /tasks/
Synonimy zakazane wewnątrz jednego API Nie zamieniamy mapy API w quest

I jest jeszcze jedna rzecz, o której łatwo zapomnieć: URI powinien być kliencki. Wewnątrz serwisu możesz przebudowywać architekturę jak chcesz (dziś serwisy, jutro osobne scenariusze, pojutrze „przepisałem wszystko na styl funkcyjny i jestem szczęśliwy"). Klientowi to obojętne. Ważne dla niego, że /api/v1/tasks zawsze oznacza to samo, niezależnie od twojej wewnętrznej filozofii.

8. Słownik nazw zasobów

Żeby API nie rozłaziło się w nazwach, słownik zasobów lepiej zafixować z wyprzedzeniem. Wtedy następny krok — zagnieżdżone zasoby — nie zamieni się w spór „jak nazwać to, czego jeszcze nie nazwaliśmy". To ważny zabieg: projekt zyskuje „oficjalną terminologię", która chroni przed przypadkowymi decyzjami „na bieżąco". Teraz nie wymieniamy całej listy endpointów ani nie omawiamy zagnieżdżeń — utrwalamy właśnie nazwy zasobów, czyli kluczowe segmenty ścieżki.

W Task Tracker API mamy cztery główne nazwy zasobów, które będziesz widywać ciągle: tasks, comments, attachments, tags. Te nazwy zostały wybrane nie dlatego, że „tak jest w internecie", lecz dlatego, że bezpośrednio odzwierciedlają domenę projektu i czytają się jednoznacznie. Gdy widzisz attachments, nie myślisz „a czy to na pewno pliki, czy to lista linków do dokumentów?", rozumiesz: to załączniki, czyli coś dołączonego do zadania.

Można to zafixować w małej „tabeli słownika":

Obiekt domenowy Nazwa zasobu (segment) Komentarz
Task tasks Główny zasób projektu
Comment comments Pomocniczy zasób obok zadań
Attachment attachments Pomocniczy zasób dla plików i metadanych
Tag tags Zasób słownikowy (bez rozbudowanego CRUD)

Ważne, że nie wymyślamy alternatywnych nazw: nie robimy todo, nie robimy work-items, nie robimy files zamiast attachments. Jedna encja — jedna nazwa. To właśnie dyscyplina kontraktowa: klient musi „nauczyć się" słownika raz i potem orientować się po mapie API bez niespodzianek.

Ścieżki w kodzie: jedno źródło prawdy

Na poziomie kodu API najczęściej „płynie" z powodu banalnego kopiowania stringów. Napisałeś "/api/v1/tasks" w jednym pliku, potem gdzieś ręcznie wpisałeś "/api/v1/task" (bez s) — i oto masz już dwa różne światy. IDE i kompilator milczą, a klient skarży się późno i boleśnie. Dlatego warto trzymać bazowy prefiks, nazwy zasobów i szablony w stylu /tasks/{taskId} w jednym miejscu.

Tu ważna nie jest konkretna klasa pomocnicza, lecz sam princip: ścieżka wybierana jest raz i potem reużywana wszędzie. Jeśli zasób nazywa się tasks, projekt nie powinien równolegle żyć z task, todo i taskList tylko dlatego, że tak wyszło w różnych plikach.

9. Typowe błędy przy projektowaniu URI

Błędy w URI rzadko łamią kod od razu, ale prawie zawsze drogo kosztują później. Kod się kompiluje, aplikacja startuje, a problem wypływa potem — gdy klient nie może znaleźć endpointu, dokumentacja zaczyna sprzeczać się z implementacją, a zespół traci czas na „dlaczego 404?". Dlatego warto zawczasu omówić typowe grabie i nauczyć się je rozpoznawać jak zapach dymu, a nie machać ręką: „no, drobiazg".

Błąd №1: kolekcja w liczbie pojedynczej.
Ścieżka w stylu /api/v1/task dla listy zadań prawie zawsze rodzi zamieszanie: „to jedno zadanie czy lista?". Liczba mnoga (/api/v1/tasks) czyni intencję oczywistą i oszczędza wyjaśnień. To małe zasada, ale wyraźnie podnosi czytelność mapy API.

Błąd №2: czasowniki w ścieżce (/getTasks, /createTask, /deleteTask).
Takie API zaczyna żyć jako katalog komend, a nie jako model zasobów. Akcja powinna żyć w metodzie HTTP, a nie w nazwie zasobu. Inaczej dostajesz podwójny sens, który z czasem zamienia się w sprzeczności i przypadkowe decyzje.

Błąd №3: kilka nazw dla tego samego sensu.
Dziś nazywasz zasób tasks, jutro wprowadzasz todo, potem pojawia się work-items. Dla klienta wygląda to jak trzy różne encje, choć sens jest jeden. Wewnątrz projektu to też niszczy dyscyplinę: pojawiają się różne DTO, różne kontrolery, różne „niemal identyczne" scenariusze. Lepiej trzymać jeden słownik i nie mnożyć synonimów.

Błąd №4: dopasowywanie URI do wewnętrznych nazw klas.
URI w stylu /api/v1/taskController/getAll może wydawać się „logiczne" dokładnie do pierwszego refaktoringu. Klient nie powinien wiedzieć, jak nazwałeś klasy i metody. Ścieżka musi wyrażać domenę i być stabilna przy zmianach implementacji.

Błąd №5: zbyt „sprytne" lub „oryginalne" ścieżki.
Czasem chce się zrobić pięknie: /api/v1/myTasks, /api/v1/tasksForMe, /api/v1/allTasks. Ale „pięknie" często oznacza „nieprzewidywalnie". Dla klienta znacznie lepiej, gdy jest bazowy zasób /tasks, a wszystkie niuanse (filtry, selekcje, przynależność) wyrażone innymi częściami kontraktu, nie łamiąc bazowego modelu zasobu.

1
Zadanie
Spring REST & MVC, poziom 4, lekcja 0
Niedostępne
Kolekcja i jeden zasób dla katalogu książek
Kolekcja i jeden zasób dla katalogu książek
1
Zadanie
Spring REST & MVC, poziom 4, lekcja 0
Niedostępne
Oznaczanie dobrych i zlych sciezek dla profili
Oznaczanie dobrych i zlych sciezek dla profili
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION