FastAPI – szybki start dla backend developera

W ostatnich latach Python stał się jednym z najważniejszych języków w świecie technologii. Wykorzystuje się go nie tylko w data science i sztucznej inteligencji, ale coraz częściej również w backendzie i budowie nowoczesnych API.

Jednym z frameworków, który bardzo szybko zdobył popularność, jest FastAPI. To nowoczesny framework do budowania API w Pythonie, który łączy wysoką wydajność z bardzo prostą składnią. Dzięki niemu można stworzyć w pełni działające API w zaledwie kilkanaście minut.

FastAPI wyróżnia się kilkoma rzeczami:

  • jest bardzo szybki (porównywalny z Node.js i Go),
  • wykorzystuje typowanie Pythona,
  • automatycznie generuje dokumentację API,
  • świetnie nadaje się do mikroserwisów i aplikacji AI.

Dla backend developerów, którzy znają PHP, JavaScript czy inne języki serwerowe, wejście w FastAPI jest zaskakująco łatwe. Kod jest czytelny, konfiguracja minimalna, a większość rzeczy działa praktycznie od razu.

W tym artykule postaram się pokazać, jak w kilka minut stworzyć pierwsze API oraz dlaczego ten framework jest tak dobrym wyborem do budowy nowoczesnych serwisów.

W tym projekcie wykorzystam Pythona w wersji 3.11.

Utworzenie środowiska wirtualnego dla nowego projektu.
Pierwszym krokiem podczas rozpoczynania pracy nad projektem w Pythonie jest utworzenie środowiska wirtualnego. Pozwala ono odseparować biblioteki wykorzystywane w danym projekcie od globalnej instalacji Pythona w systemie.

Dzięki temu każdy projekt może korzystać z innych wersji tych samych pakietów, co zapobiega konfliktom między zależnościami. Dodatkową zaletą jest możliwość łatwego odtworzenia środowiska na innym komputerze lub serwerze — wystarczy zainstalować zależności zapisane w pliku requirements.txt.

Aby utworzyć środowisko wirtualne dla projektu, należy w terminalu przejść do katalogu, w którym będzie znajdował się projekt, a następnie uruchomić polecenie:

python -m venv .venv

W systemie macOS (oraz w niektórych konfiguracjach Linuxa) może być konieczne użycie polecenia:

python3 -m venv .venv

Po utworzeniu środowiska należy je aktywować:

source .venv/bin/activate

Od tego momentu wszystkie instalowane pakiety będą trafiały do lokalnego środowiska projektu, a nie do globalnej instalacji Pythona.

Instalacja FastAPI.
Nadal pracujemy w aktywnym środowisku wirtualnym, możemy przejść do instalacji FastAPI. W terminalu należy wykonać polecenie:

pip install "fastapi[standard]"

Komenda ta instaluje framework FastAPI wraz z zestawem najczęściej używanych dodatkowych zależności. Opcja standard oznacza, że oprócz samego frameworka zostaną zainstalowane również pakiety potrzebne do wygodnego uruchamiania aplikacji w środowisku deweloperskim lub produkcyjnym — między innymi serwer ASGI Uvicorn.

Dlaczego w ogóle używamy tej opcji? Sama komenda:

pip install fastapi

również zadziała, jednak zainstaluje jedynie minimalny zestaw zależności wymaganych do działania frameworka. W praktyce oznacza to, że część bibliotek, które i tak są potrzebne w większości projektów, musielibyśmy doinstalować ręcznie.

Dlatego w przypadku nowych projektów najczęściej korzysta się z instalacji z opcją standard, która od razu przygotowuje komplet podstawowych narzędzi potrzebnych do pracy z FastAPI.

Hello FastAPI
Jeżeli instalacja pakietu FastAPI zakończyła się sukcesem, możemy przejść do napisania pierwszej aplikacji.

W katalogu projektu tworzymy plik main.py i umieszczamy w nim następujący kod:

from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def root():
  return {"message": "Hello World"}

W powyższym przykładzie tworzymy prostą aplikację oraz jeden endpoint dostępny pod adresem /. Po wywołaniu tego adresu serwer zwróci odpowiedź w formacie JSON zawierającą komunikat "Hello World".

Aby uruchomić projekt, w terminalu wpisujemy polecenie:

fastapi dev main.py

Po chwili w konsoli powinno pojawić się kilka istotnych informacji. Środowisko poinformuje nas, że serwer developerski został uruchomiony i jest dostępny pod lokalnym adresem:

http://127.0.0.1:8000

Framework FastAPI automatycznie generuje również dokumentację API w standardzie OpenAPI. Interfejs dokumentacji Swagger UI będzie dostępny pod adresem:

http://127.0.0.1:8000/docs

Otwórz przeglądarkę i przejdź do obu tych adresów w osobnych kartach. Pod pierwszym zobaczysz odpowiedź z naszego endpointu, natomiast pod drugim interaktywną dokumentację API, która pozwala testować endpointy bezpośrednio z poziomu przeglądarki.

127.0.0.1:8000

Po wejściu w przeglądarce pod adres: http://127.0.0.1:8000 powinniśmy zobaczyć następującą odpowiedź:

{
  "message": "Hello World"
}

Jak widać, efektem działania naszego endpointu jest odpowiedź w formacie JSON. Dlaczego tak się dzieje?

Ponieważ FastAPI, jak sama nazwa wskazuje, został zaprojektowany do tworzenia API, a JSON jest obecnie standardowym formatem wymiany danych pomiędzy serwerem a klientem.

Proces ten odbywa się automatycznie. Framework rozpoznaje zwracany przez funkcję obiekt Pythona (np. słownik), następnie serializuje go do formatu JSON i zwraca jako odpowiedź HTTP z nagłówkiem: Content-Type: application/json

Oczywiście nic nie stoi na przeszkodzie, aby endpoint zwracał dane w innym formacie, na przykład HTML. W tym celu musimy użyć odpowiedniej klasy odpowiedzi.

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.get("/", response_class=HTMLResponse)
async def root():
    return "<h1>Hello World</h1>"

Domyślnie FastAPI korzysta z klasy JSONResponse, dlatego w poprzednim przykładzie nie musieliśmy nic dodatkowo definiować – wszystko działo się automatycznie.

Jeżeli chcemy zwrócić HTML, musimy:

  1. wskazać ją w definicji endpointu.
  2. zaimportować klasę HTMLResponse
@app.get("/", response_class=HTMLResponse)

Parametr response_class informuje framework, że odpowiedź powinna zostać opakowana w HTMLResponse. Oznacza to, że dane będą traktowane jako HTML, a nagłówek odpowiedzi zostanie ustawiony na:

Content-Type: text/html; charset=utf-8

Gdybyśmy nie zmienili parametru response_class, framework nadal próbowałby zwrócić odpowiedź w formacie JSON.

Istnieje również drugi sposób zwracania HTML – możemy ręcznie utworzyć obiekt odpowiedzi HTTP.

@app.get("/")
async def root():
    return HTMLResponse(content="<h1>Hello World</h1>")

W parametrze content konstruktora klasy HTMLResponse przekazujemy treść HTML, która zostanie zwrócona w odpowiedzi.

Podejście manualne daje nam większą kontrolę nad odpowiedzią serwera. Oprócz treści możemy ustawić także inne elementy odpowiedzi HTTP, takie jak:

  • kod statusu,
  • nagłówki,
  • cookies.
@app.get("/")
async def root():
    return HTMLResponse(
        content="<h1>Hello World</h1>",
        status_code=200,
        headers={"X-App": "Pixelogic FastAPI Project"}
    )

Jeżeli przejdziemy teraz do dokumentacji naszego API pod adresem:

http://127.0.0.1:8000/docs

zobaczymy tam utworzony przez nas endpoint. Interfejs Swagger UI pozwala nie tylko przeglądać dokumentację, ale także testować działanie endpointów bezpośrednio z poziomu przeglądarki.

Po uruchomieniu zapytania powinniśmy otrzymać odpowiedź wygenerowaną przez naszą aplikację.


Przekazywanie parametrów w URL

Jak zapewne wiesz, przekazywanie danych w parametrach URL jest standardowym sposobem komunikacji pomiędzy warstwą frontendową a API. Budując funkcjonalności na pewno dojdziesz do momentu kiedy będziesz musiał zwrócić użytkownikowi jakieś konkretne, spersonalizowane dla niego, dane np. szczegóły jego zamówienia w sklepie internetowym. W tym celu tworzony jest adres URL zawierający dynamiczny element np ID zamówienia. FastAPI umożliwia przekazywanie parametrów w URL na dwa sposoby.

  1. Możesz przekazać dynamiczny element jako segment adresu.
  2. Możesz wykorzystać tzw Query Params.
from fastapi import FastAPI
app = FastAPI()


#   Przekazywanie dynamicznego ID zamówienia jako segmentu adresu URL: 127.0.0.1:8000/orders/1234
@app.get("/orders/{order_id}")
async def get_order_details(order_id: int):
    return {"order" : order_id}

#   Przekazywanie dynamicznego ID zamówienia jako parametru query, 127.0.0.1:8000/orders?order_id=1234
@app.get("/orders")
async def get_order_details_by_query_param(order_id: int):
    return {"order" : order_id}

Efekt działania obu endpointów może być taki sam, jednak sposób identyfikacji zasobu jest różny. Jaka jest więc między nimi różnica?

Różnica ma przede wszystkim charakter semantyczny. Przyjęło się, że parametr przekazywany w segmencie URL (tzw. path parameter) służy do identyfikacji konkretnego zasobu. W tym przypadku będzie to konkretne zamówienie klienta, np. /orders/10.

Z kolei parametry przekazywane jako query parameters służą zazwyczaj do modyfikowania sposobu zwracania danych przez serwer, a nie do identyfikacji zasobu. Często wykorzystuje się je do takich operacji jak filtrowanie, sortowanie czy paginacja. Przykładowo w parametrze ?page=2 określamy, która porcja wyników powinna zostać zwrócona.

W frameworku FastAPI oba typy parametrów są automatycznie rozpoznawane na podstawie definicji endpointu i odpowiednio parsowane oraz walidowane.

Oczywiście, możemy zastosować obie te techniki w jednym żądaniu.

#   Przekazywanie dynamicznego ID zamówienia jako parametru query, 127.0.0.1:8000/orders/1234?products_page=2
@app.get("/orders/{order_id}")
async def get_order_details_by_id_and_products_by_query_param(order_id: int, products_page: int = 1):
    print(products_page)
    return {
        "order": order_id,
        "products_page": products_page
    }

Parametr order_id przekazujemy jako segment URL a products_page jako query param.

TIPS & TRICKS : 3 dobre praktyki projektowania URL w REST API.

  1. URL powinien reprezentować zasób a nie akcję, czyli operacje należy określać metodą HTTP (GET, POST, PUT, DELETE) a nie nazwą w adresie URL : 127.0.0.1:8000/get-cart, /post-order, /delete-item-from-cart itp.
  2. Używaj hierarchii i parametrów do identyfikacji zasobów : parametry przekazywane w segmentach URL służą do identyfikacji konkretnych zasobów i ich hierarchia w adresie powinna odzwierciedlać ich wzajemną relację np.:
    GET /users/123/orders – pobierz wszystkie zamówienia użytkownika o ID 123.
    GET /users/123/orders/10 – pobierz zamówienia ID 10 użytkownika 123
  3. Stosuj konsekwentne nazewnictwo i liczby mnogie : nazwy kolekcji w adresie URL powinny być zapisane w liczbie mnogiej, używaj małych liter, poszczególne słowa oddzielaj myślnikami np.: GET /user-orders. Unikaj takich zapisów: GET /user_orders lub GET /userOrders

Przesyłanie danych za pomocą metody POST

Do tej pory przekazywaliśmy dane do endpointów metodą GET, za pomocą parametrów w URL. W praktyce bardzo często zachodzi potrzeba przesłania do API większej ilości danych, np pochodzących z formularza rejestracji lub z zamówienia w sklepie itp.

W takiej sytuacji dane przekazuje się tzw body request, czyli w treści żądania HTTP. FastAPI do definiowania modelu danych wykorzystuje bibliotekę Pydantic, która dodatkowo służy do walidowania przesłanych informacji. Aby wykorzystać potencjał Pydantic utwórzmy przykładowy model danych który walidował nam dane pochodzące z formularza logowania. W katalogu projektu utwórzmy nowy plik o nazwie schemas.py.

from pydantic import BaseModel, EmailStr

class UserLogin(BaseModel):
    email: EmailStr
    password: str

Z biblioteki Pydantic importujemy dwa bardzo ważne elementy:

  1. BaseModel – jest to klasa służąca do definiowania struktur danych oraz ich automatycznej walidacji. Dzięki niej, możemy w prosty sposób opisać, jakie dane nasz endpoint powinien otrzymać oraz jakie typu one powinny być. Jeżeli użytkownik prześle nam dane w niepoprawnej formie to BaseModel automatycznie zwróci błąd.

    Nasz klasa UserLogin dziedziczy po klasie BaseModel.
  2. EmailStr – jest to specjalny typ danych który służy do walidacji adresów e-mail. Dlaczego powinniśmy korzystać z tego typu zamiast np zwykłego str? Jak wiesz, adres e-mail ma swoją własną strukturę: nazwa – małpa – domena, zwykły typ str nie jest w stanie sprawdzić poprawności danych wg powyższego wzorca.

Zbudujmy prosty endpoint który przyjmie dane przesłane metodą HTTP POST, przeprowadzi walidację wartości oraz zwróci odpowiedni komunikat.

from fastapi import FastAPI
from schemas import UserLogin
app = FastAPI()

@app.post("/auth")
async def auth_user(userLogin: UserLogin):
    return {
        "message" : "Auth ok!",
        "user" : userLogin.email
    }

Z pliku schemas.py importujemy klasę UserLogin oraz definiujemy nowy endpoint. Wcześniej korzystaliśmy z dekoratora:

@app.get("url")

Dla metod POST dekorator wygląda troszkę inaczej:

@app.post("url")

Następnie definiujemy funkcję auth_user, która będzie obsługiwać dane przesłane w body requestu. Zwróć uwagę, że typ parametru userLogin został określony jako UserLogin. Oznacza to, że FastAPI oczekuje w treści żądania danych zgodnych ze strukturą zdefiniowaną w tym modelu.

Funkcja zwraca odpowiedź zawierającą wiadomość dla użytkownika oraz jego adres e-mail. Możemy sprawdzić działanie tego mechanizmu w dokumentacji API, przechodząc pod adres: http://127.0.0.1:8000/docs.

Po wejściu na stronę zobaczysz nowy endpoint oznaczony jako POST. Po jego rozwinięciu dostępny jest formularz z przykładowymi danymi, które możemy przesłać do API w celu przetestowania działania funkcji.

Jeżeli dane są poprawne to zobaczymy odpowiedź ze statusem 200.

Sprawdźmy teraz jak zachowa się nasz endpoint kiedy prześlemy błędne dane. Z adresu e-mail usuń małpę i ponownie prześlij dane.

Status odpowiedzi zmienił się na 422, co oznacza, że serwer przyjął żądanie, ale dane nie przeszły walidacji. Jest to zgodne z naszym oczekiwaniem – adres e-mail był niepoprawny, o czym informuje nas komunikat błędu.

Pydantic bardzo dobrze komunikuje błędy walidacji, ale nic nie stoi na przeszkodzie, aby dostosować je do własnych potrzeb i wyświetlać własne komunikaty dla użytkownika.

from pydantic import BaseModel, EmailStr, field_validator

class UserLogin(BaseModel):
    email: EmailStr
    password: str
    
    @field_validator("password")
    def check_password(cls, value):
        if len(value) < 8:
            raise ValueError("Hasło powinno zawierać min 8 znaków!")
        return value

Do modelu importujemy kolejny element z Pydantic – field_validator. W klasie UserLogin tworzymy nową metodę check_password, którą dekorujemy dekoratorem @field_validator, podając w parametrze nazwę walidowanego pola – w tym przypadku "password".

Metoda check_password przyjmuje dwa parametry:

  • cls – to klasa modelu Pydantic, czyli w naszym przypadku UserLogin. Dzięki temu w walidatorze możemy uzyskać dostęp do całego modelu i w razie potrzeby odwołać się do innych pól (np. jeśli chcielibyśmy walidować pola w kontekście siebie nawzajem).
  • value – to wartość pola, które aktualnie walidujemy, czyli w naszym przypadku wartość przesłaną w polu password.

Funkcja sprawdza, czy długość hasła jest mniejsza niż 8 znaków. Jeśli tak, rzuca wyjątek ValueError z odpowiednim komunikatem. W przeciwnym przypadku zwraca wartość pola, pozwalając na poprawną walidację.

Jeżeli chcemy nadpisać walidację adresu e-mail własną funkcją, musimy zmienić typ pola z EmailStr na str i dodać odpowiedni walidator przy użyciu dekoratora @field_validator.

from pydantic import BaseModel, EmailStr, field_validator

class UserLogin(BaseModel):
    email: str
    password: str
    
    @field_validator("password")
    def check_password(cls, value):
        if len(value) < 8:
            raise ValueError("Hasło powinno zawierać min 8 znaków!")
        return value
    
    @field_validator("email")
    def check_email(cls, value):
        if "@" not in value:
            raise ValueError("Adres e-mail ma niepoprawny format!")
        
        if not value.endswith("@example.com"):
            raise ValueError("Adres email musi kończyć się na @example.com")
        
        return value

W funkcji check_email dodaliśmy dwa warunki walidacji.

  • Pierwszy warunek sprawdza, czy ciąg tekstowy zawiera znak @. Należy jednak pamiętać, że jest to bardzo uproszczona metoda i nie jest wystarczająco wiarygodna do walidacji adresu e-mail. W praktyce powinniśmy używać bardziej precyzyjnych technik, np. sprawdzania danych za pomocą wzorca regex.
  • Drugi warunek weryfikuje, czy przesłana wartość kończy się określoną domeną lub ciągiem znaków. Dzięki temu możemy ograniczyć przyjmowane adresy do określonego formatu lub konkretnej domeny.

Takie podejście pozwala pokazać, jak można dodać własne reguły walidacji, ale w produkcyjnych aplikacjach najlepiej korzystać z gotowych mechanizmów walidacji, np. typu EmailStr w Pydantic.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *