Blog Cover Image
12 min czytania

Dlaczego nie warto ufać API?

Jeżeli jesteś doświadczonym developerem, to zapewne domyślasz się, co mam na myśli. Jeśli nie, to i tak zapraszam na krótki artykuł o tym, dlaczego warto mapować obiekty z API. Przedstawię również praktyczne przykłady, które często z powodzeniem wdrażam w swoich projektach.

Typowa historia z życia developera

Krótka historyjka, która z pewnością gdzieś na świecie codziennie ma miejsce :)

Spotkanie o 12:30, ustalony meeting frontend vs backend, żeby dogadać szczegóły kontraktu dotyczącego tworzenia użytkowników. Dogrywacie wspólnie DTO-sy. Wszystkie typy ustalone, taski rozpisane, czas na implementację.

Frontendowiec zadowolony, z pełną wiedzą co ma zrobić, przystępuje do realizacji. Robi sobie mocki na podstawie ustalonych typów, tworzy komponenty, hooki, serwisy i wreszcie integruje się z backendem - wisienka na torcie. Już zaciera ręce, żeby wrzucić taska na środowisko testowe i rozkoszować się kawusią, ale niestety nie jest tak pięknie, jak myślał.

Pierwsze problemy

Pierwszy problem - komponenty nie wyglądają tak, jak z testowymi danymi z mocków.

Zadaje sobie pytanie: jak to możliwe i co to się stało?

Niechętnie spoglądając na konsolę (myśląc już o tej przepysznej kawusi) zauważa errory. TypeScript płacze, komponenty płaczą, hooki płaczą. Krew w konsoli. No dobra, zakasuje rękawy, robi w końcu tę kawę, bo wie, że to będzie grubsza robota, z 10 minut debugowania :D

Po wstępnych oględzinach frontendowiec widzi, że backend się nie spisał i zapomniał finalnie sprawdzić czy typ się rzeczywiście pokrywa z ustalonym DTO. Akurat robił to stażysta, bo tylko on miał moce przerobowe w danym momencie i zwyczajnie nie zdawał sobie sprawy, że takie szczegóły mają znaczenie. Enumy miały być upper-case'em, a lastname zawsze stringiem, choćby pustym, ale jednak stringiem, a przyszedł undefined, itd...

// Oczekiwane UserDto
interface UserDto {
  id: number;
  firstName: string;
  lastName: string;
  role: 'ADMIN' | 'USER' | 'GUEST';
  avatarUrl: string | null;
}

// Rzeczywiste dane z API
{
  id: 1,
  firstName: "John",
  lastName: undefined,
  role: "admin",
  avatarUrl: undefined
}

Naprawiamy, ale...

Frontendowiec wziął się w garść, pół godzinki, DTO ponownie uzgodnione, typy naprawione, wszystko się ładnie wyświetla. Poszło do QA, zostało zaakceptowane. Działa.

Dwa dni później znowu się wysypało - drugi backendowiec, co refactorował obrazki, zapomniał zmienić struktury danych dla obrazków. Frontendowiec dostał już ticketa z bugiem, bo przecież to front nie wyświetla obrazków :)

I znowu szukanie, debugowanie, czerwono w konsoli... Pół godziny debugowania i jest! Okazuje się, że obrazkowi brakowało jednego property, drobny błąd. Poprawione. Znowu działa.

Problem z perspektywy czasu

Temat był świeży, debugowanie poszło sprawnie i błąd został szybko wykryty. Co jeśli taki przypadek zdarzyłby się za pół roku, a ten nieszczęsny obrazek byłby używany w 50 innych typach danych?

Rozwiązanie tego problemu i wielu innych problemów sugeruje już początek tego wpisu. Są to mappery. Czasami też nazywane transformersami, adapterami, jak zwał tak zwał - ja nazywam te funkcje mapperami.

// Przykład prostego mappera
const assureString = (value: unknown): string => {
  if (typeof value === 'string') return value;
  if (value === undefined || value === null) return '';
  return String(value);
};

Dlaczego to problem?

Jeśli byśmy przyjęli, że typ pochodzący z API to po prostu UserDto, to trochę sami prosimy się o katastrofę. Dlaczego?

  • ta struktura wcale nie musi być tego typu, dopóki się nie upewnimy i nie zapewnimy, że jest tego typu
  • ciężko nam później pracować z obiektem, który jest "niepewny" i jesteśmy zmuszeni ładować pytajniki "?", żeby TypeScript jednak sprawdził, czy dane property rzeczywiście istnieje
  • nie wiemy, w którym miejscu w aplikacji się coś wysypało, a czasami znalezienie takiego błędu w dużym projekcie kosztuje pół dniówki średnio doświadczonego developera
  • nie oddzielamy warstwy tego, co pochodzi "z zewnątrz" od tego, co mamy "wewnątrz" aplikacji
  • piszemy bardziej skomplikowane interfejsy dla komponentów, niż moglibyśmy
  • piszemy niespójne interfejsy dla komponentów, bo raz enum przyjdzie camelCasem, a raz UPPER_CASEM
  • nie stosujemy wewnętrznych typów aplikacji, bo przecież mamy DTO-sy, to po co robić drugi, bardzo podobny typ

Co nam dają mappery?

  • przede wszystkim z czystym sumieniem możemy poinformować TypeScript, że zadeklarowany typ to prawda
  • pozwala nam utrzymać spójne struktury danych wewnątrz aplikacji
  • jak backend coś zmieni, świadomie lub nie, będziemy o tym od razu dobrze poinformowani
  • od razu mamy walidator danych, czy typ, na który się umówiliśmy w kontrakcie, jest prawidłowo zaimplementowany
  • aplikacja nie wysypie się przy braku głupiego property
  • mamy spójne dane, nawet gdy są różne źródła danych

Kiedy stosować mappery?

Stosujemy mappery, gdy dane pochodzą z zewnętrznego źródła - w tym przypadku z API. Innym zewnętrznym źródłem może być chociażby zawartość cookie czy LocalStorage. Przecież każdy może wejść w dev toolsy i zamiast obiektu wstawić string, czy zamiast booleana - number, a to nam może również wysypać aplikację i spowodować, że będzie ona działać niestabilnie.

Kiedy odpuścić mappery?

To kiedy sobie odpuścić takie mappery?

  • jak piszesz fullstackowo aplikację sam dla siebie
  • jak robisz proste MVP, które zaraz i tak "zaorasz"
  • gdy ORM (np. TypeOrm) robi to za nas :)

Podsumowanie

Ten artykuł stanowi pierwszą część z serii o mapowaniu danych z API. W kolejnej części:

  • Poznasz praktyczne implementacje mapperów dla różnych przypadków użycia
  • Zobaczysz jak radzić sobie z zagnieżdżonymi obiektami
  • Dowiesz się jak obsługiwać tablice obiektów
  • Poznasz techniki cachowania zmapowanych danych
  • Nauczysz się pisać testy dla mapperów
  • Zobaczysz przykłady wykorzystania generycznych typów w mapperach
  • Poznasz popularne biblioteki do mapowania danych

Zachęcam do eksperymentowania z własnymi implementacjami mapperów i dostosowywania przedstawionych rozwiązań do swoich potrzeb.

Objaśnienie skrótów

  • API - Application Programming Interface - Interfejs Programowania Aplikacji
  • DTO - Data Transfer Object - Obiekt Transferu Danych, struktura danych służąca do przesyłania informacji między systemami
  • QA - Quality Assurance - Zapewnienie Jakości, proces weryfikacji jakości oprogramowania
  • MVP - Minimum Viable Product - Minimalny Możliwy Produkt, wersja produktu z minimalnym zestawem funkcji
  • ORM - Object-Relational Mapping - Mapowanie Obiektowo-Relacyjne, technika konwersji danych między systemami typów w bazach danych
  • TypeORM - Biblioteka ORM dla TypeScript i JavaScript, ułatwiająca pracę z bazami danych
  • ADMIN - Administrator - użytkownik z najwyższymi uprawnieniami w systemie
  • UI - User Interface - Interfejs Użytkownika, warstwa wizualna aplikacji
  • UPPER_CASE - Konwencja nazewnictwa używająca wielkich liter i podkreśleń (np. USER_NAME)
  • camelCase - Konwencja nazewnictwa zaczynająca się małą literą, gdzie kolejne słowa zaczynają się wielką literą (np. userName)

Linkografia do dalszej exploracji :)

Konrad Bysiek - avatar

Konrad Bysiek

Frontend Developer / Tech Lead