REST API w Pythonie: Flask czy FastAPI?

Tworzenie aplikacji internetowych, a w tym REST API, to chleb powszedni backend developerów. Dlatego praca z frameworkiem webowym powinna być szybka i prosta. Microframeworki to bardzo dobry start dla małych projektów, MVP czy nawet dużych aplikacji, które potrzebują REST API – a do nich zaliczają się m.in.: Flask i FastAPI.

Flask jest jedną z najpopularniejszych bibliotek do tworzenia aplikacji internetowych w Pythonie. Osoby zaczynające swoją przygodę z programowaniem bez trudu znajdą na jego temat mnóstwo tutoriali i rozwiązań typowych problemów. Jest on lekki (“microframework”) i bardzo dobrze udokumentowany. Posiada wiele rozszerzeń i sporą społeczność. 

FastAPI robi się coraz bardziej popularny z dnia na dzień. Jego nacisk na szybkość (FastAPI), nie tylko w kwestii ilości obsługiwanych zapytań na sekundę, ale również na szybkość developmentu oraz wbudowaną walidację danych – tworzy z niego idealnego kandydata na backendową stronę naszej aplikacji internetowej.

Napisałem aplikację do tworzenia, aktualizowania, pobierania oraz usuwania wiadomości prasowych w dwóch wyżej wymienionych frameworkach i bardzo chętnie przedstawię Wam ich porównanie.

Pierwszą znaczącą różnicą, którą możemy znaleźć przyglądając się tym dwóm bibliotekom to:

Walidacja danych

Instalując Flaska nie dostajemy żadnego narzędzia do walidacji danych. Możemy jednak poradzić sobie przy użyciu dodatków, które oferuje społeczność, np. Flask-Marshmallow czy Flask-Inputs. Minusem tego rozwiązania jest fakt, że  musimy polegać na bibliotekach rozwijanych oddzielnie niż nasz główny framework i nie mamy stuprocentowej pewności, że będą one kompatybilne.

FastAPI natomiast daje nam do użytku bibliotekę Pydantic, dzięki której walidacja danych jest o wiele prostsza i szybsza niż pisanie tego z palca. Jest ona ściśle związana z samym FastAPI, więc możemy być pewni, że Pydantic będzie cały czas kompatybilny z naszym frameworkiem.

Jak wyglądają walidacje w poszczególnych bibliotekach na podstawie naszego prostego API?

Tworzymy klasy o nazwach `NewsSchema`/`CreatorSchema`, które będą klasami bazowymi do walidacji naszych wiadomości oraz autorów.

Możemy zauważyć, że `NewsSchema`/`CreatorSchema` pochodzące z FastAPI używają `BaseModel` jako klasy nadrzędnej – jest to wymagane, gdyż `BaseModel` pochodzi z bilbioteki Pydantic i posiada niezbędne funkcje do walidacji danych. 

 

Natomiast we Flasku dziedziczymy po klasie `BaseSchema`, która jest zwykłą dataklasą i zawiera parę metod, z których klasy dziedziczące będą korzystać lub ją nadpisywać.

 

W naszym przypadku sprawdzimy jedynie czy tekst, który wprowadzamy, mieści się w limicie znaków. 

 

Sama walidacja będzie zachodzić w klasach `NewsSchemaInput`/`CreatorSchemaInput`:

Gdy stworzymy nasz obiekt `NewsSchemaInput`/`CreatorSchemaInput`, uruchomiona zostanie metoda `__post_init__`, w której po kolei wywołujemy walidację danych (sprawdzanie długości tekstu) i jeżeli jest ona niepoprawna – dodajemy błędy do zmiennej `_errors`, a na końcu rzucamy wyjątkiem `Validation Error`.

 

W przypadku struktur, które są zagnieżdżone (`CreatorSchemaInput`), musimy manualnie tworzyć te obiekty – robimy to po skończonej walidacji `NewsSchemaInput` w metodzie `__post_init__`.

 

Samo sprawdzanie danych nie stanowi większego problemu – dopiero dodawanie nowych pól będzie uciążliwe, ponieważ za każdym razem musimy dodać osobną metodę `_validate` oraz w przypadku zagnieżdżonej struktury – tworzenie instancji tego obiektu i łapanie wyjątku.

Możemy zauważyć, że klasy, które mają za zadanie walidować przychodzące dane, stają się dosyć rozbudowane – i to tylko dla paru kluczy. Musieliśmy również dodać własną implementację dodawania błędów, dzięki czemu możemy dodawać zagnieżdżone informacje o błędach w odpowiedziach z API.

W FastAPI jest to o wiele prostsze i przyjemniejsze:

Poprzez zaimportowanie `Field` z `Pydantic` mamy dostęp do prostych deklaracji zasad, które muszą zostać spełnione, aby wprowadzane przez użytkownika dane były prawidłowe.

Również typy danych są walidowane na podstawie typów zmiennych, więc jeżeli nasza zmienna `first_name` posiada typ `str`, to musimy w danych wejściowych przekazać tekst (i analogicznie dla wszystkich wbudowanych typów danych).

Bez dodatkowego kodu Pydantic świetnie radzi sobie ze sprawdzaniem zagnieżdżonych struktur (w tym przypadku `CreatorSchemaInput`).

I to wszystko w paru linijkach kodu!

Oprócz `max_length` i `min_length` możemy zauważyć też dwa dodatkowe parametry: 

`title` oraz `example` – Są one opcjonalne, ale będą widoczne w automatycznej dokumentacji, którą generuje za nas FastAPI.

Serializacja danych wychodzących

Skoro wiemy już, jak walidujemy dane, to powinniśmy zastanowić się, jak chcemy je zwracać. Wiadomość będzie posiadała  nie tylko treść, tytuł czy autora, ale również swój unikalny numer (id) oraz datę utworzenia i aktualizacji. Musimy stworzyć nową klasę, która będzie odpowiadała za serializację modelu domenowego `News` i będzie to `NewsSchemaOutput`.

Klasa `NewsSchemaOutput` w obydwu przypadkach jest praktycznie taka sama, różni się tylko klasą nadrzędną oraz metodą serializacji do słownika (razem ze zmianą obiektu `datetime` na timestamp).

 

W FastAPI przy użyciu Pydantic mamy możliwość dodania klasy `Config`, w której umieściliśmy zmienną `json_encoders`. Pomaga ona serializować dane w sposób, którego wymagamy. W tym przypadku obiekt daty chcemy przekazać jako timestamp. We Flasku natomiast musieliśmy zmieniać dane w stworzonym już słowniku na takie, które chcemy zwrócić.

Widoki aka endpoints

Tworzenie widoków w obu bibliotekach jest bardzo do siebie podobne i wykorzystuje prosty dekorator na funkcji, której chcemy użyć. Różnią się natomiast sposoby definiowania walidacji danych oraz ich serializacji.

Tworzenie wiadomości, czyli walidacja i serializacja danych

Na samym początku mamy dekorator, który ustala ścieżkę dostępu oraz metodę HTTP, która będzie obsługiwana. Flask ustala to za pomocą parametru `methods`, gdzie musimy przekazać listę obsługiwanych metod, natomiast FastAPI używa atrybutu `post` na `news_router`.

 

Dekorator, którego używa FastAPI, nie służy jedynie do ustalania ścieżki i metod HTTP, ale również do serializacji danych (`response_model`), opisania widoku w automatycznej dokumentacji (`summary`), zdefiniowania statusu odpowiedzi (`status_code`) oraz wielu innych, nie znajdujących się w tym przykładzie. Można powiedzieć, że nie definiuje on jedynie ścieżki dostępu i metody, ale opisuje cały widok w głębi.






No dobrze, ale co się w tym widoku tak naprawdę dzieje?

 

Zacznijmy od Flaska!

 

Pierwszą rzeczą, którą robimy, jest pobranie repozytorium bazy danych do naszej funkcji za pomocą:

W następnym kroku walidujemy przesłane dane przez użytkownika, które znajdują się w obiekcie `request`:

Ta linijka rzuci wyjątkiem `ValidationError`, jeżeli wprowadzone dane są nieprawidłowe. 

Wyjątek zostanie wyłapany (w stworzonym przez nas `errorhandler`) i Flask zwróci odpowiedź ze wszystkimi błędami, które znajdują się w zmiennej `_errors` na `NewsSchemaInput`.

 

> W stworzonym przez nas `errorhandler` – ale zaraz, zaraz, nie było o tym mowy!

We Flasku i FastAPI możemy dodać własne obsługiwanie wyjątków, które zostaną 

rzucone w implementacji widoków. Wyglądają one tak:

Jeżeli walidacja przebiegła poprawnie, tworzymy obiekt `NewsDTO`, który przekaże niezbędne informacje do repozytorium bazy danych. Repozytorium wykona swoją magię (zapisze wiadomość do bazy danych) i zwróci nam obiekt domenowy `News`, który następnie serializujemy za pomocą klasy `NewsSchemaOutput`:

Na samym końcu zwracamy `NewsSchemaOutput` jako słownik oraz status odpowiedzi.

Teraz rzućmy okiem na FastAPI.

 

Tym razem w widoku dostajemy dwa parametry: `news_input` oraz `db_repo`.

Zacznijmy od tego pierwszego:

 

Walidacja danych wejściowych dzieje się jeszcze przed uruchomieniem metody naszego widoku, a to dzięki parametrowi `news_input`.

Zapytacie: ale skąd FastAPI wie, której klasy użyć? Dzięki typowaniu. Parametr `news_input` posiada typ `NewsSchemaInput`, więc FastAPI przekazuje tej klasie wszystkie dane, które wysłaliśmy metodą POST. Nie musimy specjalnie tworzyć instancji obiektu `NewsSchemaInput`, ponieważ w parametrze `news_input` dostaniemy zwalidowane dane.

 

Odnośnie `db_repo` – działa on podobnie jak we Flasku – z tą różnicą, że tutaj używamy wstrzykiwania zależności. Słowo kluczowe `Depends` pozwala na podstawianie klas lub funkcji podczas działania naszej aplikacji. Odnośnie `dependency injection` troszkę później.

Gdy nasza metoda zostanie wywołana, zapisujemy wiadomość do bazy danych.

We Flasku musieliśmy specjalnie tworzyć instancję klasy `NewsSchemaOutput`, żeby zwrócić poprawne dane. Tak samo ze statusem odpowiedzi, jest on również odesłany za pomocą słowa kluczowego `return`.

 

FastAPI pozwala na sprecyzowanie klasy do serializacji danych za pomocą parametru `response_model` w dekoratorze. Wystarczy, że podamy prawidłową strukturę, którą zrozumie `Pydatnic`. Status odpowiedzi również możemy ustawić w tym samym miejscu co `response_model`, ale za pomocą parametru `status_code`.

Pobieranie wiadomości, zmienne w adresie i parametry GET

 

Tak samo jak w przypadku tworzenia wiadomości, definiujemy widok za pomocą prostego dekoratora, lecz tym razem z metodą GET.

Aby pobrać interesującą nas wiadomość, musimy naszemu widokowi przekazać jego id. Robimy to za pomocą adresu, w którym dodajemy parametr `news_id`. We Flasku musimy szczegółowo podać jego typ za pomocą “ostrych” nawiasów oraz nazwę, czyli `<int:news_id>`. Jesteśmy zmuszeni do używania tylko podstawowych typów, które Flask zrozumie np. int, uuid, str lub float i tak dalej.

 

FastAPI używa używa konwencji podobnej co f-string, gdzie nazwę naszej zmiennej definiujemy poprzez nawiasy klamrowe, a jej typ ustalamy w parametrach funkcji widoku. Jest to rozwiązanie bardziej elastyczne, gdyż możemy spróbować przekazywać skomplikowane struktury w adresie. Mogliście również zauważyć nowy parametr, który pojawił się w dekoratorze widoku o nazwie `responses` – wrócimy do niego podczas omawiania automatycznej dokumentacji.

FIltrowanie wiadomości za pomocą parametrów GET

 

Gdy chcemy stworzyć widok, który nie potrzebuje zdefiniowanych zmiennych w adresie, a bardziej elastyczne rozwiązanie – używamy parametrów GET. W tym przypadku potrzebujemy zwrócić wiadomości, które spełniają kryteria przekazane do nas za pomocą tzw. `query parameters`. Mamy do dyspozycji dwa parametry: `id` oraz `created_at`.

Flask oferuje obiekt `request`, z którego możemy wyciągnąć wszystkie dane odnośnie zapytania do naszego widoku. Tym razem interesują nas parametry `id` oraz `created_at`. Wiemy również, że możemy spodziewać się listy tych parametrów – w tym celu używamy metody `getlist` ze specjalnego słownika `args`. 

Następnie przesyłamy wyciągnięte dane do repozytorium bazy danych, aby otrzymać listę modeli domenowych `News`, którą zamieniamy w zestawienie słowników z klasy `NewsSchemaOutput`. 

Musimy jeszcze pamiętać, że nie możemy zwrócić listy z widoku – konieczne jest wykonanie funkcji `jsonify`, aby nasz endpoint zwrócił obiekt `Response` z prawidłową serializacją listy.

Z FastAPI cały proces wygląda dosyć podobnie jak we Flasku – z tą różnicą, że zmienne adresowe dostajemy w parametrach funkcji, co jest o wiele czytelniejsze niż wykonywanie `request.args.getlist …` z każdą zmienną, którejpotrzebujemy. Aby FastAPI wiedział, że parametry funkcji to zmienne adresowe – musimy dodać im domyślną wartość `Query`, która jest z góry zdefiniowana.

 

Skąd FastAPI wie, że oczekujemy specyficznego typu danych, jeżeli nie sprecyzowaliśmy go w nawiasach klamrowych? Z typowania.

Wystarczy, że dodamy typ do naszych parametrów np. `Set[int]`, a będziemy pewni, że w zmiennej znajdzie się zbiór tylko i wyłącznie z liczbami całkowitymi.

Po walidacji zmiennych adresowych wyciągamy modele domenowe `News` z repozytorium bazy danych poprzez przesłane kryteria. Następnie zwracamy listę słowników modeli wiadomości, a `response_model` w dekoratorze zajmie się prawidłową serializacją danych.

Wstrzykiwanie zależności

Wstrzykiwanie zależności – wzorzec projektowy i wzorzec architektury oprogramowania polegający na usuwaniu bezpośrednich zależności pomiędzy komponentami.

 

Brzmi dosyć skomplikowanie, prawda? Otóż FastAPI był w stanie zaimplementować ten wzorzec w bardzo prosty sposób.

 

Mogliśmy zauważyć, że w każdym widoku w parametrach funkcji znajduje się coś takiego:

Jest to nic innego jak wstrzyknięcie zależności – w tym przypadku wstrzykujemy repozytorium bazy danych. Słowo kluczowe `Depends` jest w stanie wstrzykiwać wszystko, co może być wywołane (np. klasy czy funkcje). Jest to o tyle dobry sposób, że pozwala na zachowanie reguły DRY (Don’t Repeat Yourself), ponieważ nie musimy za każdym razem tworzyć nowej zmiennej dla repozytorium bazy danych jak we Flasku:

Kolejną zaletą `Depends` jest łatwe podstawianie implementacji w testach.
We Flasku, żeby zamienić zwracaną wartość z `get_database_repo`, musielibyśmy mockować tę funkcję za każdym razem, kiedy uruchamialibyśmy testy.

Dzięki wstrzykiwaniu zależności w FastAPI. możemy użyć

aby zamienić implementację podczas uruchamiania testów.

 

`Depends` może być również używany jako niepowtarzanie tych samych parametrów funkcji n razy. Po więcej odsyłam do dokumentacji.

 

Asynchroniczność

Flask niestety nie obsługuje asynchroniczności oraz interfejsu ASGI co oznacza, że niektóre długo działające zapytania mogą blokować naszą aplikację. Wiąże się to z mniejszą ilością użytkowników, którą możemy obsłużyć naszym REST API.

 

Pewnie zauważyliście, że funkcje widoków w FastAPI zaczynają się od `async`, a każde wywoływanie metody na repozytorium bazy danych poprzedzone jest słówkiem `await`.

 

FastAPI jest w pełni asynchroniczne (co nie znaczy, że jest to wymagane – możemy również implementować zwykłe synchroniczne funkcje) i używa interfejsu ASGI, dzięki czemu możemy używać nieblokujących zapytań do baz danych czy zewnętrznych serwisów, a ilość jednoczesnych użytkowników korzystających z naszej aplikacji będzie o wiele większa niż w przypadku Flaska.

 

FastAPI w swojej dokumentacji ma bardzo dobrze rozpisany przykład dotyczący używania `async` i `await` – polecam z całego serca go przeczytać: https://fastapi.tiangolo.com/async/

 

To może uruchomimy jakiś benchmark?

 

Do tego zadania użyjemy Locust – jest to darmowe narzędzie do testowania obciążeniowego wszelakich API napisane w Pythonie z otwartym źródłem. Nasz test będzie opierał się na dodawaniu co sekundę 100 użytkowników do puli aktywnych połączeń, aż dotrzemy do dwóch tysięcy użytkowników jednocześnie.

Jak możemy zauważyć, liczba zapytań na sekundę, jaką możemy obsłużyć to około 633.

Nie jest aż tak źle, prawda? Mogło być jednak lepiej. Średni czas oczekiwania na odpowiedź to około 1642 ms – praktycznie półtorej sekundy, aby otrzymać jakiekolwiek dane z API to zdecydowanie za dużo. Do tego dochodzi 7% nieudanych zapytań.

FastAPI poradziło sobie o wiele lepiej w tym zadaniu. Liczba zapytań, którą możemy obsłużyć to około 1150 na sekundę (prawie dwa razy więcej niż we Flasku), a średni czas oczekiwania na odpowiedź to zaledwie… 14ms. Wszystkie zapytania były poprawne i nie otrzymaliśmy żadnych błędów.

Automatyczna dokumentacja

Podczas tworzenia REST API dokumentacja jest niezbędna zespołowi programistów lub użytkowników, którzy chcą używać tego interfejsu do komunikacji z naszą aplikacją. Można poradzić sobie tworząc ją manualnie, np. w Jira Confluence/Github wiki lub innym narzędziu do zbierania projektowych danych. Jest to jednak obarczone ryzykiem błędu ludzkiego, gdy np. ktoś zapomni zaktualizować adresy do widoków lub zrobił literówkę.

 

Najczęstszym standardem tworzenia takich dokumentacji jest OpenAPI i JSONSchema.

 

We Flasku dostępne są dodatki np. Flask-Swagger czy Flasgger, które operują na wyżej wymienionej specyfikacji. Wymagają one dodatkowej instalacji i poznania formatu, którym posługują się te standardy.

 

Również specyfikacje przesyłanych danych muszą zostać napisane manualnie – nie będą one pobierane z klas, które walidują lub parametrów, które pobieramy.

 

FastAPI posiada dokumentację w pełni kompatybilną z OpenAPI oraz JSONSchema, która tworzy się automatycznie ze schematów Pydantic i parametrów funkcji czy zmiennych GET. Interfejs użytkownika jest dostarczony przez SwaggerUI oraz Redoc.

 

Jest to bardzo ciekawa funkcja, gdyż nie wymaga od nas żadnej pracy (chyba, że chcemy upiększyć naszą dokumentację o szczegóły). Wszystkie zasady dotyczące wymaganych danych znajdują się w schematach Pydatnic.

 

Dokumentacja dostępna jest pod adresem `host/doc` (SwaggerUI) oraz `host/redoc`(ReDoc) i wygląda w ten sposób:

W SwaggerUI mamy również dostęp do wszystkich schematów, które zdefiniowaliśmy w naszej aplikacji:

W SwaggerUI mamy również dostęp do wszystkich schematów, które zdefiniowaliśmy w naszej aplikacji:

 

Możemy zauważyć, że pojawiły się informacje z parametrów `summary` i `title` z `CreatorSchemaInput`.

 

Skąd FastAPI wie, jakie informacje przekazać do dokumentacji? Przyjrzyjmy się na przykładzie pobierania wiadomości:

W dekoratorze znajdują się parametry, które będą uwzględnione podczas tworzenia dokumentacji:

  • `/news/{news_id}` – W dokumentacji zobaczymy, że parametr `news_id` jest wymagany i musi być liczbą całkowitą.
  • `response_model` – Ten schemat odpowiedzi zostanie automatycznie wyświetlony w dokumentacji.
  • `responses` – Jeżeli nasz widok zwraca inne kody odpowiedzi niż 200/400/422 lub 500, możemy dodać specjalny słownik z rozpisanymi statusami oraz schematem zwracanych danych tak jak tutaj:

Również docstring jest brany pod uwagę i zostanie on pokazany jako dodatkowa informacja konkretnego widoku. 

Podsumowanie

Przyjrzeliśmy się dzisiaj dwóm świetnym bibliotekom na przykładzie bardzo prostej aplikacji CRUD z REST API. Z jednej strony mamy bardzo popularnego Flaska, obok którego nie można przejść obojętni z drugiej FastAPI, które podbija serca użytkowników ilością wbudowanych funkcjonalności oraz asynchronicznością. Jeżeli miałbym wybierać framework na mój kolejny projekt związany z interfejsem REST, na pewno moje oczy skierowały by się w kierunku FastAPI. Mam nadzieję, że kiedy będziecie zastanawiać się nad kolejnym frameworkiem, to chociaż spróbujecie dać FastAPI szansę. Aplikację, którą napisałem możecie sprawdzić tutaj.

Przeczytaj również

Najciekawsze w Praca w STX Next

Nasz przepis na skuteczne wdrożenie Junior Developerów – STX Next Crash Course

Wejście do nowej organizacji, pierwsza praca czy zmiana pracy w momencie, gdy nie ma się jeszcze dużego doświadczenia, może być bardzo stresujące. Wszyscy doskonale pamiętamy swoje pierwsze zawodowe kroki, dlatego w STX Next bardzo mocno stawiamy na wysokiej jakości proces wdrożenia. W tym celu właśnie powstał STX Next Crash Course dla Junior Developerów, co jest […]

Praca Solutions Architecta oczami Produktowca

Jarek Feith pracuje z nami od kilku miesięcy jako Product Solutions Consultant. Swoim wieloletnim doświadczeniem produktowym wspiera Solutions Architectów – programistycznych ekspertów do zadań specjalnych. Jak wygląda ta współpraca oczami Jarka? O tym możecie przeczytać poniżej: Standardowa ścieżka kariery dla senior developera to najczęściej przejście na poziom zarządzania działem, zespołem – generalnie praca bardziej z […]

Roślinny Tinder w React Native – mariaż dwóch pasji

Programowanie w React Native to sposób, by wynieść swoje pasje typowo offline do zupełnie nowego poziomu. Nasz Hrad of React Native jest twórcą popularnej aplikacji do wymiany roślin, Plantswapp.

Czytaj więcej

Kontakt

Masz pytania?