Strona główna / Artykuły

Powszechne błędy w umowie API, które psują niezawodność frontendu

Dziesięć powszechnych błędów w umowie API, które niszczą niezawodność frontendu – od zmieniających się formatów odpowiedzi i statusu 200 OK przy każdym błędzie po kruche mechanizmy paginacji, wraz z rozwiązaniami.

Wydawca: REACTAPP.TOP18 min czytania4119 słów

Źródła: 10 API Design Mistakes That Make Frontend Developers Hate You

API może wyglądać doskonale na papierze, a mimo to sprawić, że wszyscy, którzy na nim budują, będą cierpieć.

Endpoint zwraca ważny JSON. Kod stanu jest poprawny. Odpowiedź przychodzi szybko. Z perspektywy backendu wydaje się, że zadanie zostało ukończone. Inżynierowie frontendu widzą coś zupełnie innego. Każda próba połączenia wymaga dodatkowej weryfikacji bezpieczeństwa. Endpointy, które wyglądają podobnie, zwracają zupełnie różne dane. Komunikaty o błędach przypominają linie z dziennika, a nie coś, co powinien widzieć użytkownik. Funkcja paginacji zachowuje się niespójnie w zależności od danego zasobu. Nie ma niezawodnego sposobu, by odróżnić wartość pustą od brakującej lub brakującą od nieudanej prośby.

Całe to rozczarowanie nie wynika z faktu, że deweloperzy frontendu oczekują od backendu dostosowywania każdej odpowiedzi do konkretnego komponentu. Dobrze zaprojektowana API nie musi odzwierciedlać żadnego określonego interfejsu użytkownika. Potrzebuje natomiast wystarczającej klarowności, aby wiele różnych interfejsów mogło tworzyć przewidywalne zachowanie w jej oparciu.

Jasne umowy API ograniczają domysły. Określają one, co się udało, a co nie, które pola są opcjonalne, w jaki sposób sortowane są wyniki, czy istnieją dodatkowe strony oraz jakie wartości można bezpiecznie zapamiętywać w pamięci tymczasowej lub porównywać z upływem czasu. Słabe umowy przenoszą całą tę niejednoznaczność na zewnątrz, zmuszając każdą stronę internetową, aplikację mobilną, panel administracyjny oraz integrację z podmiotami trzecimi do tworzenia własnej interpretacji.

Wtedy właśnie pojawia się niezadowolenie. Frontend przestaje traktować API jako źródło prawdy i zaczyna postrzegać je jako coś, co trzeba obejść.

1. Zwracanie różnej struktury odpowiedzi z każdego punktu końcowego

Jeden punkt końcowy zwraca prosty tablicę. Inny umieszcza swoją zawartość wewnątrz klucza data. Trzeci łączy pola result, message i success. Trasa z paginacją przechowuje swoje rekordy pod kluczem items, natomiast sąsiednia trasa używa do tego samego celu klucza rows. Jeśli usuniesz obiekt za pośrednictwem jednego kontrolera, otrzymasz usunięty rekord; jeśli za pośrednictwem innego – nic w ogóle.

Rozpatrując każdy przypadek oddzielnie, żadna z tych odpowiedzi nie jest trudna do zrozumienia. Problemy pojawiają się, gdy zespół front-endu próbuje stworzyć wspólną, wielokrotnie używalną logikę do komunikacji z całym API.

Nie ma sposobu, by z pewnością napisać jeden helper do zapytań, mechanizm obsługi błędów czy adapter do paginacji, ponieważ każdy endpoint wymaga własnego rozwiązania. Komponenty muszą sięgać do arbitralnie głębokich, niestabilnych struktur tylko po to, by wydobyć jakąś wartość:

const users = response.data.result.users;
const orders = response.items;
const reports = response.data.rows;

Oczywistym kosztem jest powtarzanie się kodu mapowania. Głębszym kosztem jest nieprzewidywalność. Gdy programista front-end napotka endpoint, którego wcześniej nie używał, nie ma sposobu, by od zachowania reszty API odgadnąć jego strukturę.

Konsekwencja nie oznacza, że każda odpowiedź musi zawierać identyczne pola. Jedna zasoba, lista zasobów, wynik paginowany, rezultat polecenia oraz zadanie asynchroniczne oznaczają różne rzeczy, więc jest uzasadnione, że ich struktury się różnią. Ważne jest to, by każda z tych kategorii miała strukturę, którą front-end może od razu rozpoznać.

Odpowiedź typu lista zawsze może zawierać swoje rekordy wraz z pewnymi metadanymi. Polecenie zawsze może zwrócić albo zaktualizowany zasób, albo wyraźnie zdefiniowany obiekt wyniku. Błędy zawsze mogą mieć określoną, udokumentowaną strukturę. Stany puste mogą być reprezentowane w ten sam sposób we wszystkich podobnych punktach końcowych.

Jednolity projekt odpowiedzi sprawia, że infrastruktura frontendu pozostaje prosta. Nielojalny projekt zamienia każdy ekran w specjalnie dostosowany adapter, skonfigurowany tak, aby odpowiadał temu konkretnemu deweloperowi backendu, który napisał dany szlak.

2. Używanie 200 OK dla każdego wyniku

Część API odpowiada kodem 200 OK, niezależnie od tego, co faktycznie się wydarzyło, ukrywając prawdziwy wynik w treści odpowiedzi:

{
  "success": false,
  "message": "User not found"
}

To wzorzec zwykle powstaje jako skrót mający na celu uproszczenie obsługi po stronie klienta: każda prośba jest rozstrzygana w ten sam sposób, a interfejs użytkownika jedynie sprawdza flagę success w ciele żądania.

W praktyce pozbawia to sensu usługi, które warstwa transportowa już oferuje za darmo.

Klienci HTTP, narzędzia monitoringu, pamięci cache, proxy’y, API przeglądarek oraz biblioteki do pobierania danych są wszystkie zbudowane wokół kategorii kodów stanu. Odrzucone żądanie może naturalnie przechodzić w stan błędu. Pomyślne utworzenie można odróżnić od zwykłego odczytu. Konflikt można obsłużyć inaczej niż nieprawidłowe dane wejściowe lub brakujący token autoryzacji.

Gdy każda odpowiedź przychodzi jako 200, frontend musi pamiętać o sprawdzaniu treści ciała przy każdym wywołaniu. Jeśli to zaniedba, nieudana operacja może wyglądać, jakby się udała. Ogólna logika ponawiania prób staje się ryzykowna, ponieważ klient nie jest już w stanie odróżnić tymczasowej awarii serwera od błędu walidacji. Utrudnia się również obserwowalność, gdy panele kontrolne pokazują falę udanych żądań, podczas gdy użytkownicy napotykają w rzeczywistości problemy.

Błędem przeciwnym jest łączenie wszystkich awarii w jeden kod 500. Brakujący rekord, błędny żądanie, problem z uprawnieniami, duplikat wartości oraz awaria w łańcuchu dostaw to nie ten sam rodzaj awarii. Umieszczanie ich wszystkich w jednym ogólnym błędzie serwera zmusza frontend do analizy tekstu komunikatu, aby dowiedzieć się, co tak naprawdę poszło nie tak.

Kody stanu nigdy nie miały przenosić pełnego znaczenia biznesowego odpowiedzi, ale powinny dokładnie klasyfikować jej wynik. Ciało odpowiedzi może wtedy zawierać wszelkie strukturyzowane szczegóły, których faktycznie potrzebuje interfejs.

Frontend nie powinien musieć analizować frazy „adres e-mail już istnieje”, aby zrozumieć, że mamy do czynienia z konfliktem.

3. Wysyłanie tekstu błędu bez żadnej struktury podstawowej

Zwykła wiadomość o błędzie może wystarczyć osobie przeglądającej logi, ale rzadko dostarcza interfejsowi wystarczających informacji do dalszej pracy.

Załóżmy, że punkt końcowy rejestracji odpowiada coś w rodzaju:

{
  "message": "Validation failed"
}

Na podstawie tego klient nie ma sposobu, by dowiedzieć się, które pole spowodowało problem, która zasada została złamana, czy kilka pól zawiodło jednocześnie, a nawet czy ten błąd w ogóle dotyczy formularza.

W obliczu tego problemu programiści zmuszeni są tworzyć kruchą logikę porównywania ciągów znaków: jeśli w tekście pojawia się słowo „email”, do pola wprowadzania adresu e-mail dodaje się błąd; jeśli pojawia się słowo „password”, wyświetla się ostrzeżenie związane z hasłem. Gdy tylko serwer zmieni sformułowanie, poprawi błąd ortograficzny, zmieni wielkość liter lub doda lokalizację, ta logika w milczeniu przestaje działać.

Lepszym podejściem jest oddzielenie tego, co maszyna musi zrozumieć, od tego, co człowiek musi przeczytać. Odpowiedź może zawierać stabilny kod błędu, szczegóły dla poszczególnych pól oraz informacje kontekstowe o żądaniu, oprócz komunikatu, który można bezpiecznie wyświetlić lub zapisać.

Gdy ta struktura jest już gotowa, frontend może decydować o tym, jak każdy błąd powinien wyglądać na ekranie. Błędy walidacji na poziomie pola mogą być pokazywane obok odpowiedniego pola wprowadzania danych. Niepowodzenie autoryzacji może spowodować przekierowanie lub ukrycie nieaktywnej opcji. Błąd konfliktu może zaproponować użytkownikowi sposób na jego rozwiązanie. Tymczasowy błąd usługi może zachować to, co użytkownik wpisywał, i zaoferować przycisk ponownej próby zamiast usuwać jego prace.

To wszystko nie oznacza wyrzucania surowych śladów wyjątków lub ciągów znaków błędów z bazy danych do odpowiedzi — one zazwyczaj ujawniają wewnętrzne szczegóły implementacji i i tak powodują zamieszanie. Zadaniem backendu jest przekształcanie błędów niskiego poziomu w stabilne, dobrze zdefiniowane błędy domeny.

Dobrze zaprojektowany komunikat błędu pozwala klientowi odpowiedzieć na trzy podstawowe pytania: do której kategorii błędów należy, do której części interfejsu się odnosi oraz czy użytkownik ma jakikolwiek sposób, by z nim zareagować.

Jeśli pominąć taką strukturę, każdy frontend kończyć będzie tworzeniem własnej, niepewnej interpretacji tekstu, który przypadkowo zwróci backend.

4. Traktowanie null, brakujących pól, pustych ciągów znaków i pustych tablic jako wymiennych

Każda opcjonalna wartość w API wymaga precyzyjnego określenia znaczenia jej braku.

Pole, którego brakuje w treści przesyłanej, może oznaczać po prostu to, że nie zostało ono zażądane. Wartość null może oznaczać, że system wyraźnie wie, iż nic tam nie ma. Pusta strona tekstowa może oznaczać, że użytkownik zdecydował się nie wpisywać tekstu, albo może to być jedynie objaw tego, że starszy kod źle radzi sobie z obsługą brakujących wartości. Pusta tablica może oznaczać, że rzeczywiście nie ma żadnych powiązanych elementów, albo może to oznaczać, że załadunek relacji nie powiódł się i została ona po cichu zastąpiona pustym списком.

Gdy API łączy te formy reprezentacji w sposób niejednolity, interfejs użytkownika nie ma żadnego pewnego sposobu, by dowiedzieć się, z jaką dokładnie sytuacją ma do czynienia.

Pole profilu może zostać całkowicie pominięte w jednej odpowiedzi i pojawić się jako null w innej. Koniec listy może zwracać null zamiast pustego tablicy, gdy nie ma nic do pokazania, co oznacza, że każdy komponent musi sprawdzić dane w sposób defensywny przed ich przetworzeniem. Data, która nie jest dostępna, może pojawić się jako pusta strona, co zakłóca działanie bibliotek do parsowania oczekujących ważnego formatu. Relacja, której nie udało się załadować, może zostać w tajemnicy wyświetlona jako pusta tablica, przez co prawdziwy błąd wygląda dokładnie jak normalny stan pustki.

Powodem tego jest kod frontendu pełen działań defensywnych zmierzających do normalizacji danych, na przykład:

const records = Array.isArray(response.records)
  ? response.records
  : [];

const displayName =
  response.displayName ?? response.name ?? "";

Pewna ilość normalizacji jest uzasadniona — systemy zewnętrzne są z natury niepewne, a klientom należy się przed tym chronić. Problem pojawia się wtedy, gdy interfejs użytkownika nie radzi sobie już z prawdziwą niepewnością pochodzącą ze źródeł zewnętrznych, lecz próbuje obejść to poprzez modyfikację umowy API, która nie potrafi określić, co oznacza „nic”.

Rozwiązaniem jest to, aby API celowo zdefiniowało, co reprezentuje każdy rodzaj braku. Zbiory powinny pozostać zbiorem nawet wtedy, gdy nie zawierają żadnych elementów, zamiast zamieniać się w null. Pola, które mogą legalnie być puste, powinny mieć możliwość wartości null celowo, a nie przypadkowo. A gdy pole zostanie całkowicie pominięte, powinien istnieć udokumentowany powód tego faktu — na przykład rzadkie zestawy pól, ograniczenia uprawnień lub specyficzne projekcje dla danego punktu końcowego.

Gdy brak ma już określone znaczenie, powstałe stany interfejsu stają się bardziej przejrzyste. Frontend może odróżnić sytuacje takie jak „nie ma komentarzy”, „komentarze nie zostały włączone do tej odpowiedzi” oraz „nie udało się załadować komentarzy”.

Są to trzy zupełnie różne doświadczenia użytkownika, mimo że na pierwszy rzut oka wszystkie mogą wyglądać identycznie: pusta sekcja komentarzy, w której nic się nie pokazuje.

5. Udostępnianie surowych modeli bazy danych bezpośrednio przez API

Serializacja entity ORM wprost i jej wysłanie jako odpowiedzi może wydawać się najprostszym rozwiązaniem – obiekt już znajduje się w pamięci, więc po co tworzyć dodatkową warstwę tylko po to, by go przekształcić?

Problem polega na tym, że wiersz bazy danych i kontrakt API istnieją z różnych powodów.

Tabela może zawierać wewnętrzne kody stanu, kolumny kluczy obcych, daty czasowe dotyczące delikatnego usuwania danych, metadane audytu, ścieżki przechowywania lub pozostałe kolumny z poprzedniej migracji. Serializacja tej struktury bezpośrednio łączy każdego klienta z decyzjami dotyczącymi przechowywania danych, o których w ogóle nie powinien wiedzieć.

Gdy frontend zaczyna polegać na polach takich jak user_role_id, deleted_at lub na złożonej strukturze relacji, która wynika po prostu z konfiguracji określonego ORM, backend znajduje się w pułapce. Późniejsza zmiana nazwy kolumny, podział tabeli, modyfikacja zachowania ładowania danych asynchronicznego lub migracja do innego usługi – wszystko to, co powinno być czysto wewnętrznymi modyfikacjami, nagle staje się zmianą zakłócającą działanie publicznego API.

Serializacja entytetów bezpośrednio stwarza również ryzyko przypadkowego ujawnienia danych. Ktoś dodaje nową kolumnę w bazie danych, a ponieważ domyślny serializator obejmuje wszystkie pola, one pojawiają się w odpowiedziach API bez żadnej decyzji o tym ze strony twórców. Notatki wewnętrzne, wrażliwe metadane lub wartości istotne dla bezpieczeństwa mogą w ten sposób wyciec, bez żadnej celowej decyzji projektowej.

Dedykowany model odpowiedzi rozwiązuje ten problem, pełniąc rolę granicy. Określa dokładnie, na których polach klienci mogą polegać, przekształca reprezentacje na poziomie przechowywania w zrozumiałe koncepcje domeny i zapobiega przypadkowemu wyciekowi struktury wewnętrznej do publicznego interfejsu.

To nie wymaga ręcznego pisania ogromnej funkcji mapowania dla każdego pojedynczego punktu końcowego. Serializatory typowe, narzędzia oparte na schematach oraz użyteczne funkcje mapowania mogą znacznie zmniejszyć ilość powtarzań. Ważne jest, aby struktura odpowiedzi była świadomym wyborem, a nie skutkiem przypadkowego wyniku zwracanego przez ORM.

Rozwijający interfejs użytkownika powinni móc tworzyć rozwiązania w oparciu o stabilne koncepcje produktowe, a nie o aktualną strukturę tabeli w bazie danych.

6. Tworzenie paginacji, na której nikt nie może polegać

Paginacja wydaje się prosta, dopóki dane ulegają zmianie w trakcie przewijania ich przez użytkownika.

Koniec punktu końcowego przyjmuje stronę i limit, stosuje odstęp, a następnie zwraca fragment rekordów. Frontend renderuje elementy sterujące stroną i przewija listę. Wszystko to działa sprawnie, dopóki nie są dodawane nowe wiersze, nie usuwane stare lub dopóki kolejność sortowania nie jest ustalona.

Załóżmy, że użytkownik przegląda pierwszą stronę. W tym czasie na początku kolekcji dodawany jest nowy rekord. Gdy użytkownik poprosi o drugą stronę, element, który już widział na pierwszej stronie, pojawi się ponownie, ponieważ odstęp uległ zmianie. W innym miejscu inny rekord może zostać całkowicie pominięty i w ogóle nie zostać pokazany.

Sytuacja pogarsza się, gdy endpoint od samego początku nie gwarantuje stabilnego porządku. Wtedy silnik bazy danych może zwracać wiersze w dowolnej kolejności, jaką preferuje jego planer zapytań. Dwie identyczne prośby, wysłane jedna po drugiej, mogą zwrócić wiersze w innej kolejności, mimo że nic istotnego w danych się nie zmieniło.

To nie jest coś, co frontend może samodzielnie naprawić. Wszystko zależy od tego, czy backend dostarczy deterministyczny porządek wraz z metadanymi paginacji, które rzeczywiście mają znaczenie.

Paginacja oparta na odstępach w połączeniu z gwarantowaną kolejnością sortowania sprawdza się dobrze przy wielu zestawach danych. Jednak w przypadku źródeł danych, które często się zmieniają, lub bardzo dużych kolekcji, paginacja oparta na wskaźniku zazwyczaj lepiej radzi sobie w takich warunkach. Wybór odpowiedniego podejścia zależy od produktu, ale niezależnie od tego, które wybierzesz, umowa musi jasno określić, w jaki sposób klient przemieszcza się po kolekcji.

Odpowiedź dotycząca paginacji musi również dostarczyć klientowi odpowiedzi na kilka praktycznych pytań. Czy istnieje następna strona? Jaka wartość wskaźnika powinna być użyta w następnym żądaniu? Czy całkowita liczba jest dokładna, szacunkiem, czy po prostu celowo nie podana? Co powinno się stać, jeśli strona, którą obecnie przegląda klient, stanie się pusta z powodu usunięcia elementów?

Gdy punkt końcowy zwraca dane bez żadnego niezawodnego sposobu na ich nawigację, interfejs użytkownika musi wyświetlać elementy sterujące stronicowaniem, którym nie może w pełni ufać. Użytkownicy widzą duplikaty i brakujące wpisy, a za problem, który w rzeczywistości wynika z błędu w umowie, obwinia się interfejs.

7. Wysyłanie dat bez określenia strefy czasowej

Data prawie nigdy nie jest zwykłą ciągłą literami.

Czasowa oznaczenie bez podania strefy czasowej może odnosić się do lokalnego czasu serwera, czasu przechowywania w bazie danych, lokalnego czasu samego użytkownika lub wartości, która miała być w formacie UTC, ale straciła swój offset podczas formatowania. W takiej sytuacji interfejs użytkownika musi samodzielnie wyświetlać, porównywać, sortować i uruchamiać określone działania na podstawie wartości, której rzeczywiste znaczenie jest tylko częściowo określone.

Weźmy na przykład odpowiedź w postaci "2026-08-04 09:00:00". Nie wskazuje ona, do którego dokładnego momentu odnosi się ta data. Sesja przeglądarki w Pakistanie i sesja w Kanadzie mogą interpretować tę sekwencję znaków inaczej, a zmiany czasu letniego dodatkowo zwiększają niepewność.

Jako zasada ogólna, daty czasowe bezwzględne powinny zawierać wyraźną informację o odchyleniu lub być przedstawione w standardowym formacie UTC. Wartości kalendarzowe, które nie są powiązane z konkretnym momentem, takie jak data urodzenia czy data fakturowania, powinny pozostać zwykłymi wartościami datowymi i nie powinny być cicho przesuwane między strefami czasowymi.

To rozróżnienie ma rzeczywiste konsekwencje. Jeśli przeliczymy datę urodzin przechowywaną jako północ UTC na czas lokalny, niektórzy użytkownicy zobaczą niewłaściwy dzień – ten poprzedni. Z drugiej strony, jeśli potraktujemy zaplanowane wydarzenie jako zwykły ciąg znaków reprezentujący tylko datę, wszyscy będą widzieć, że ma ono miejsce o godzinie wskazanej przez ich lokalny zegar, a nie o tej samej wspólnej porze.

API powinno również zachowywać spójność pod względem dokładności i formatu używanego we wszystkich punktach końcowych. Problemem jest sytuacja, gdy jeden punkt końcowy zwraca sekundy Unixa, inny milisekundy, a trzeci jakiś specyficzny format ciągu znaków.

Rozwijający interfejs użytkownika potrafią sformatować datę zgodnie z ustawieniami lokalnymi użytkownika. Nie mogą jednak odtworzyć strefy czasowej, o której API w ogóle im nie poinformowało.

Błędy związane z strefami czasowymi są szczególnie irytujące, ponieważ zazwyczaj działają poprawnie dla zespołu backendu, który pracuje w swojej strefie czasowej, a problemy pojawiają się tylko u użytkowników znajdujących się gdzie indziej.

8. Przymuszanie prostych interfejsów do łączenia danych poprzez łańcuchy żądań

Strona pobiera aktualnego użytkownika. Gdy użytkownik wraca, strona pobiera informacje o jego organizacji. Po uzyskaniu danych o organizacji pobierane są uprawnienia. Dopiero po otrzymaniu uprawnień strona pobiera rekordy, które użytkownik faktycznie ma prawo widzieć.

Mimo że każdy pojedynczy punkt końcowy odpowiada szybko, cały interfejs może potrzebować kilku sekund, zanim pokaże coś przydatnego.

Taki wzorzec pojawia się zazwyczaj wtedy, gdy API jest projektowane wyłącznie wokół znormalizowanych zasobów serwera, bez większej uwagi na to, w jaki sposób klient faktycznie odczytuje te dane w praktyce. Projekt po stronie serwera wygląda schludnie i uporządkowanie, podczas gdy klient musi sam zorganizować cały sekwencyjny proces, by wyświetlić jedną stronę.

Rozwijający interfejs użytkownika reagują na to, układając stanów ładowania jednych wewnątrz innych. Awaria w jakimś momencie tego łańcucha może spowodować, że ekran zostanie tylko częściowo zainicjowany. Obsługa anulowania staje się skomplikowana, gdy użytkownik przechodzi do innej strony w trakcie pobierania danych. Ponadto ta sama informacja jest często żądana kilka razy, ponieważ poszczególne komponenty mają po jednym ogniwie w tym łańcuchu.

Rozwiązaniem nie jest wyświetlanie całej bazy danych za pomocą jednego megapunktu końcowego. Zamiast tego serwer powinien rozpoznawać istotne ścieżki klienta i udostępniać modele do odczytu zaprojektowane tak, by efektywnie im służyć.

Na przykład punkt końcowy do panelu sterowania może od razu zwrócić dane podsumowawcze oraz flagi funkcjonalne niezbędne do jego działania. Punkt końcowy z detalami rekordu może zawierać małe, powiązane obiekty, które zawsze są wymagane podczas przeglądania. W przypadku danych rzeczywiście niezależnych od siebie rozsądnym rozwiązaniem mogą być również żądania równoległe.

Zapytania w stylu grafowym, warstwy backend-for-frontend, parametry rozszerzania zasobów oraz specjalnie zaprojektowane punkty końcowe to wszystko skuteczne narzędzia w tym celu. Konkretny styl architektoniczny ma mniejsze znaczenie niż rezultat: standardowe ekran nie powinny zmuszać przeglądarki do stopniowego badania grafu relacji serwera, żądanie po żądaniu.

API nie zyskuje lepszych ocen pod względem projektu tylko dlatego, że każdy punkt końcowy odpowiada dokładnie jednej tabeli w bazie danych. Dobrze zaprojektowany kontrakt musi również uwzględniać to, w jaki sposób dane są faktycznie wykorzystywane po drugiej stronie.

9. Ukrywanie zasad biznesowych za zielonym światłem

Czasami punkt końcowy służący do modyfikacji zgłasza sukces, mimo że ostateczny wynik jeszcze nie został w pełni ustalony.

Zapytanie o raport trafia do kolejki w celu jego wygenerowania w tle. Płatność przechodzi w stan oczekiwania. Operacja usuwania jest zaplanowana na później, po upływie określonego okna przechowywania. Dostawca zewnętrzny potwierdza otrzymanie zapytania, a nie to, że ukończył pracę.

Gdy API odpowiada tym samym obiektem zawierającym informację o sukcesie, który używa w przypadku rzeczywiście zakończonej pracy, interfejs użytkownika narażony jest na podanie użytkownikowi informacji, które jeszcze nie są prawdziwe.

Interfejs może wyświetlać komunikat „Raport gotowy”, mimo że jest nadal w trakcie tworzenia. Może usunąć wiersz z listy, chociaż serwer tylko oznaczył go do usunięcia w przyszłości. Może pokazać, że płatność została sfinalizowana, podczas gdy dostawca może ją jeszcze odrzucić później.

Klienci potrzebują sposobu na odróżnienie operacji, które zostały zakończone, zaakceptowane, wciąż w toku, częściowo udane lub wymagające dalszych działań. Nie jest to błahe szczegółowe kwestie implementacyjne — bezpośrednio wpływa na to, co interfejs może uczciwie przekazać użytkownikowi.

Dla operacji wymagających realizacji w czasie rzeczywistym często warto modelować je jako odrębną pracę lub zasób zadania, zawierający identyfikator oraz pole stanu. Stany znajdujące się w jakimś pośrednim stanie powinny być wyraźnie pokazane, zamiast były łączone w ogólne „sukces”. Gdy wynik jest częściowy, odpowiedź powinna dokładnie określić, co się udało, a co nie.

Ta sama dyscyplina przynosi korzyści przy ponawianiu prób. Jeśli żądanie wygasło, ale serwer backend może je już przetwarzać, interfejs frontend nie powinien po prostu wysyłać żądania ponownie i mieć nadziei. Posiadanie identyfikatora operacji lub klucza idempotencji umożliwia klientowi bezpieczne ponawianie prób lub sprawdzanie stanu, bez ryzyka powtórnego wystąpienia efektów ubocznych.

Odpowiedź o sukcesie powinna odzwierciedlać jedynie tę gwarancję, za którą backend może obecnie ręczyć — nic więcej.

Jest to poważne źródło frustracji, gdy inżynierowie frontendu muszą sami decydować, czy status 200 oznacza zakończenie zadania, jego jedynie rozpoczęcie, czy po prostu „nic nie spowodowało błędu w trakcie wykonywania”.

10. Rozwijanie umowy bez planu dla starych klientów

Zmiana API wydaje się prosta, gdy istnieje pojedyncza gałąź frontendu rozwijana synchronicznie z nim. Zmienia się nazwę pola, dostosowuje kod klienta, wysyła obie wersje razem i przechodzi dalej.

Ruch produkcyjny rzadko funkcjonuje w ten sposób.

Ktoś może zostawić otwartą starą kartę przeglądarki podczas aktualizacji. Aplikacja mobilna może pozostać zainstalowana na urządzeniu przez miesiące bez aktualizacji. Integracja z podmiotem trzecim może odebrać twoje zmiany dopiero w ramach swojego harmonogramu wydawania aktualizacji. Stary, skompresowany plik JS może nadal wysyłać żądania do nowego backendu, oczekując przy tym starej struktury odpowiedzi.

Usunięcie lub zmiana nazwy pola może w tajemnicy zepsuć kod, którego nikt w zespole backendowym nie ma możliwości natychmiastowej naprawy.

Część najbardziej ryzykownych zmian na pierwszy rzut oka nie wygląda na takie. Zastąpienie wartości typu enum może w tajemnicy zepsuć logikę warunkową interfejsu użytkownika. Zamiana pola z nieprzyjmującego wartości pustych na przyjmujące może naruszyć założenia wbudowane w generowane typy. Zmiana domyślnego porządku sortowania może spowodować rozregulowanie strony paginacji. Wprowadzenie nowego statusu błędu może skierować żądanie zupełnie inną ścieżką obsługi błędów po stronie klienta.

Zarządzanie tym problemem zaczyna się od zrozumienia, kto faktycznie korzysta z API. Klienci wewnętrzni, które są wdrażane synchronicznie z backendem, zazwyczaj mogą tolerować szybszą iterację niż integracje skierowane do publiczności — ale nawet czysto wewnętrzne systemy korzystają z zmian stopniowych oraz procesu stopniowego wycofywania starych pól.

Praktyczne taktyki obejmują: wprowadzanie nowych pól przed wycofywaniem starych, śledzenie rzeczywistych danych dotyczących użycia, aby wiedzieć, kiedy można bezpiecznie coś usunąć, przeprowadzanie testów kontraktów z kluczowymi użytkownikami oraz stosowanie odpowiedniego wersjonowania API, gdy semantyka zmienia się zbyt fundamentalnie, by można było ją łagodnie rozwijać na miejscu.

To wszystko nie oznacza, że każdy historyczny kontrakt musi być wspierany wiecznie. Chodzi o to, by nie pozwalać na to, by problemy ze strony klienta stały się „problemem kogoś innego” tylko dlatego, że backend został wdrożony pierwszy.

Gdy inne systemy zaczną polegać na API, jego zachowanie ma cykl życia, który wykracza daleko poza kod, który go pierwotnie zaimplementował.

Bądźmy przyjazni frontendowi – to nie znaczy pozwalać mu dyktować zasady API

Dobrze zaprojektowana API to nie taka, która po prostu odzwierciedla formę, w której dany komponent React najlepiej funkcjonuje. To tylko wprowadziłoby inną, równie kruchą formę powiązania.

Zamiast tego oferuje stabilne znaczenie w kontekście domeny, przewidywalną strukturę, uczciwe raportowanie wyników oraz wystarczająco dużo kontekstu, aby klienci mogli stworzyć poprawne rozwiązania bez konieczności zgadywania. Wyraźnie odróżnia brak danych od całkowitego awarii, prace w toku od zakończonych zadań, problemy z walidacją od konfliktów oraz sposób wyświetlania czegoś od sposobu jego przechowywania.

Zespoły frontendu doskonale radzą sobie z prawdziwą złożonością — paginacja, stany błędów, przejścia podczas ładowania, przepływy asynchroniczne to rzeczy, które budują na co dzień. To, co naprawdę wyczerpuje ludzi, to przypadkowa złożoność, powstająca wskutek niestabilnych struktur odpowiedzi i braku sygnałów semantycznych, a nie samego problemu.

Najlepsze relacje między backendem a frontendem opierają się na wspólnym projektowaniu tych umów. Frontend określa, które różnice faktycznie wpływają na zachowanie interfejsu dla użytkownika. Backend wyjaśnia, jakie gwarancje może bezpiecznie przyjąć oraz które operacje naturalnie rozgrywają się w dłuższym czasie. Wynikająca z tego umowa API nie należy w całości do żadnej ze stron.

API budzi niezadowolenie w momencie, gdy zaczyna przekazywać na zewnątrz własną niepewność.

Każdy niejednoznaczny kod stanu przekształca się w warunkowy rozgałęzienie na stronie klienta. Każda niespójna struktura odpowiedzi staje się kolejną funkcją adaptacyjną. Każdy brakujący uporządkowany kod błędu prowadzi do niestabilnego porównywania ciągów znaków. Każde niestabilne pole wymaga dodatkowej obronnej weryfikacji wartości null.

To wszystko nie oznacza, że dobrze zaprojektowana API eliminuje całą pracę frontendu.

Zamiast tego sprawia, że deweloperzy frontendu mogą poświęcić swój czas na faktyczną budowę produktu, zamiast ciągle próbować odtworzyć intencje backendu.

Literatura pokrewna

  • Dlaczego kod backendu, który działa lokalnie, zawodzi przy rzeczywistym obciążeniu produkcji — Praktyczny przegląd założeń dotyczących środowiska, bazy danych, bezpieczeństwa i niezawodności, które sprawnie funkcjonują na localhost, ale powodują przerwy w działaniu, gdy napływa rzeczywisty ruch do środowiska produkcyjnego.
  • Anty-patterny backendu: 7 drożnych błędów i praktyczne sposoby ich naprawy — Dowiedz się, jak wykrywać i naprawiać siedem powszechnych błędów w inżynierii backendu, od nadmiernie rozbudowanych kontrolerów po błędy nierozwiązane, zanim spowodują one problemy w produkcji.