1 FastAPI PL
teacher edited this page 2026-04-13 15:12:56 +02:00

FastAPI — Przewodnik po zadaniach

Niniejszy przewodnik omawia wszystkie dziesięć zadań z katalogu fastapi-assignments/. Każda sekcja wyjaśnia nie tylko jak uruchomić kod, ale przede wszystkim dlaczego projekt jest skonstruowany w taki, a nie inny sposób. Materiał jest przeznaczony dla osób znających podstawy Pythona, które nigdy wcześniej nie budowały webowego API.


Wymagania wstępne

Przed przystąpieniem do jakiegokolwiek zadania należy upewnić się, że poniższe elementy są przygotowane.

Wersja Pythona

FastAPI wymaga Pythona 3.8 lub nowszego. Sprawdź zainstalowaną wersję:

python --version

Środowiska wirtualne

Środowisko wirtualne to izolowana instalacja Pythona, która oddziela zależności jednego projektu od zależności innego. Każde zadanie należy uruchamiać we własnym środowisku wirtualnym.

Tworzenie i aktywacja środowiska:

# Utwórz środowisko (jednorazowo dla każdego folderu zadania)
python -m venv venv

# Aktywacja na macOS/Linux
source venv/bin/activate

# Aktywacja na Windows
venv\Scripts\activate

Gdy środowisko jest aktywne, monit powłoki zmienia się — pojawia się przedrostek (venv). Środowisko należy aktywować przed każdą instalacją zależności i przed uruchomieniem serwera.

Narzędzia do testowania

FastAPI automatycznie generuje interaktywną dokumentację. W większości zadań wystarczy otworzyć przeglądarkę pod adresem http://127.0.0.1:8000/docs. Dla większej kontroli warto zainstalować Postman lub korzystać z curl w terminalu.


Zadanie 01: Proste API do zarządzania zadaniami (To-Do)

Lokalizacja: fastapi-assignments/01-basic-todo-api/

Co się nauczysz

  • Jak tworzyć aplikację FastAPI i definiować trasy
  • Cztery podstawowe metody HTTP: GET, POST, PUT, DELETE
  • Jak walidować dane wejściowe za pomocą modeli Pydantic
  • Jak działają parametry ścieżki (path parameters)
  • Czym jest przechowywanie danych w pamięci operacyjnej i jakie ma ograniczenia

Kluczowe pojęcia

Metody HTTP odpowiadają operacjom na zasobie. Zgodnie z przyjętą konwencją: POST tworzy zasób, GET go odczytuje, PUT zastępuje lub aktualizuje, a DELETE usuwa. FastAPI używa dekoratorów (@app.get, @app.post itd.) do powiązania funkcji z konkretną metodą i ścieżką URL.

Pydantic to biblioteka do walidacji danych, która korzysta z mechanizmu type hints Pythona. Gdy definiujesz klasę dziedziczącą po BaseModel, FastAPI używa jej do automatycznej walidacji request body. Jeśli klient prześle błędne dane — na przykład liczbę zamiast ciągu znaków — FastAPI zwróci odpowiedź 422 Unprocessable Entity, zanim Twój kod w ogóle zostanie wywołany.

Parametry ścieżki to zmienne fragmenty adresu URL. W ścieżce /todos/{todo_id} segment {todo_id} jest przechwytywany i przekazywany do funkcji. FastAPI konwertuje go również do zadeklarowanego typu: adnotacja todo_id: int sprawi, że ciąg znaków "5" z URL stanie się liczbą całkowitą 5 w Pythonie.

Instalacja

pip install fastapi uvicorn[standard]

Uvicorn to serwer ASGI (Asynchronous Server Gateway Interface) — protokół, który łączy FastAPI z siecią. Aby uruchomić aplikację FastAPI, zawsze potrzebujesz serwera ASGI.

Uruchamianie serwera

uvicorn main:app --reload

Polecenie mówi Uvicornowi, aby zaimportował obiekt app z pliku main.py. Flaga --reload powoduje automatyczny restart serwera przy każdej zmianie pliku, co jest bardzo wygodne podczas pracy deweloperskiej.

Pliki projektu

schemas.py

Plik definiuje trzy modele Pydantic:

  • TodoCreate — kształt danych przesyłanych przez klienta podczas tworzenia zadania. Zawiera pole title (niepusty ciąg znaków) oraz opcjonalne description.
  • TodoUpdate — kształt danych przesyłanych podczas edycji zadania. Wszystkie pola są opcjonalne, ponieważ powinna być możliwa aktualizacja częściowa — na przykład zmiana samego tytułu.
  • TodoResponse — kształt danych zwracanych przez serwer. Rozszerza podstawowe pola o id (nadawane przez serwer) i completed (wartość logiczna).

Podział na trzy modele jest celowy. Klient nie powinien mieć możliwości ustawiania pola id ani flagi completed podczas tworzenia zadania — są to pola kontrolowane przez serwer. Rozdzielenie modeli wejściowych i wyjściowych wymusza tę granicę.

main.py

Aplikacja przechowuje zadania na zwykłej liście Pythona (todos: list = []) oraz w liczniku identyfikatorów (next_id: int = 1). Dane te istnieją wyłącznie w pamięci RAM. Po zatrzymaniu procesu wszystkie dane są tracone.

Każda funkcja obsługująca trasę:

  1. Odbiera zwalidowane dane żądania przekazane przez FastAPI
  2. Operuje na liście w pamięci
  3. Zwraca dane (które FastAPI serializuje do JSON) lub zgłasza wyjątek HTTPException

HTTPException to mechanizm FastAPI służący do zwracania odpowiedzi błędów. Wywołanie raise HTTPException(status_code=404, detail="Not found") natychmiast przerywa obsługę żądania i odsyła klientowi odpowiedź 404.

Testowanie krok po kroku

  1. Uruchom serwer: uvicorn main:app --reload
  2. Otwórz http://127.0.0.1:8000/docs
  3. Kliknij POST /todos, następnie "Try it out". Wprowadź body w postaci {"title": "Kupić zakupy"} i kliknij "Execute". Zanotuj id w odpowiedzi.
  4. Kliknij GET /todos i wykonaj żądanie. Powinieneś zobaczyć swoje zadanie na liście.
  5. Kliknij PUT /todos/{todo_id}. Wprowadź id z kroku 3 i body w postaci {"title": "Kupić zakupy i mleko"}.
  6. Kliknij DELETE /todos/{todo_id} z tym samym id.
  7. Wykonaj ponownie GET /todos. Lista powinna być pusta.

Częste problemy

  • ModuleNotFoundError: No module named 'fastapi' — środowisko wirtualne nie jest aktywowane lub zależności nie zostały zainstalowane.
  • Zajęty port — inny proces używa portu 8000. Zatrzymaj go lub uruchom serwer z opcją uvicorn main:app --reload --port 8001.

Zadanie 02: Osobisty blog

Lokalizacja: fastapi-assignments/02-personal-blog/

Co się nauczysz

  • Jak integrować relacyjną bazę danych za pomocą SQLAlchemy
  • Czym jest ORM (Object-Relational Mapper)
  • Dependency injection w FastAPI
  • Czym są URL slug i dlaczego mają znaczenie
  • Jak dane są utrwalane między restartami serwera

Kluczowe pojęcia

SQLAlchemy to zestaw narzędzi SQL i ORM dla Pythona. Warstwa ORM pozwala na interakcję z bazą danych za pomocą klas i obiektów Pythona, bez konieczności pisania surowego SQL. Definiujesz klasę (Post), a SQLAlchemy tłumaczy Twoje operacje Pythona na instrukcje SQL.

SQLite to relacyjna baza danych oparta na plikach. W odróżnieniu od baz serwerowych (PostgreSQL, MySQL) SQLite przechowuje wszystko w jednym pliku (blog.db). Nie wymaga żadnej instalacji ani konfiguracji, co czyni ją idealną do nauki.

Dependency injection to wzorzec projektowy, w którym funkcja deklaruje, czego potrzebuje (swoje zależności), a framework dostarcza je w momencie wywołania. W FastAPI zależności deklaruje się za pomocą Depends. Funkcja get_db tworzy sesję bazy danych, przekazuje ją do obsługi trasy, a następnie ją zamyka — nawet jeśli w trakcie obsługi wystąpił wyjątek.

Slug to przyjazny dla URL identyfikator wywodzący się z czytelnego dla człowieka tekstu. Dla wpisu zatytułowanego "Mój pierwszy post" slug przyjąłby postać moj-pierwszy-post. Slugi nadają URLom znaczenie i stabilność. Są zazwyczaj zapisane małymi literami, oddzielone myślnikami i pozbawione znaków specjalnych.

Instalacja

pip install fastapi uvicorn[standard] sqlalchemy

Uruchamianie serwera

uvicorn main:app --reload

Przy pierwszym uruchomieniu SQLAlchemy automatycznie tworzy plik blog.db i tabelę posts.

Pliki projektu

database.py

Plik zawiera trzy elementy:

  1. engine — połączenie z plikiem SQLite. Ciąg sqlite:///./blog.db oznacza "SQLite, ścieżka względna, plik o nazwie blog.db".
  2. SessionLocal — fabryka tworząca sesje bazy danych. Każde żądanie HTTP otrzymuje własną sesję.
  3. get_db — funkcja generatorowa używana jako zależność FastAPI. Blok try/finally gwarantuje zamknięcie sesji nawet w przypadku błędu podczas obsługi żądania.

models.py

Definiuje klasę Post odwzorowaną na tabelę posts. Wywołanie Base.metadata.create_all(bind=engine) w main.py odczytuje wszystkie klasy dziedziczące po Base i tworzy odpowiadające im tabele, jeśli jeszcze nie istnieją.

schemas.py

Zawiera modele Pydantic: PostCreate (wejście) i PostResponse (wyjście). Zwróć uwagę na model_config = ConfigDict(from_attributes=True) w PostResponse. Ta konfiguracja informuje Pydantic, że może odczytywać dane z atrybutów modelu SQLAlchemy (nie tylko ze słowników), co umożliwia płynną konwersję między obiektem ORM a odpowiedzią JSON.

Pole slug w PostCreate używa ograniczenia regex: pattern=r'^[a-z0-9]+(?:-[a-z0-9]+)*$'. Zapewnia to akceptowanie wyłącznie poprawnych slugów — wyrażenie regularne wymaga małych liter lub cyfr, z myślnikami jedynie między segmentami.

main.py

Handlery tras otrzymują db: Session = Depends(get_db). FastAPI wywołuje get_db, pobiera obiekt sesji i przekazuje go do handlera. Po zwróceniu odpowiedzi przez handler FastAPI wznawia wykonanie get_db, aby zamknąć sesję.

Endpoint GET /posts/{slug} używa db.query(Post).filter(Post.slug == slug).first(). Jeśli żaden wpis nie pasuje, .first() zwraca None, a handler zgłasza wyjątek 404.

Testowanie krok po kroku

  1. Uruchom serwer.
  2. Utwórz wpis: POST /posts z body {"title": "Witaj świecie", "slug": "witaj-swiecie", "content": "Mój pierwszy wpis."}.
  3. Pobierz go: GET /posts/witaj-swiecie.
  4. Zatrzymaj i uruchom ponownie serwer. Następnie pobierz wpis jeszcze raz. Nadal tam jest — w odróżnieniu od zadania 01, dane zostały utrwalone.
  5. Spróbuj utworzyć drugi wpis z tym samym slugiem. Serwer powinien zwrócić błąd 409 Conflict, ponieważ slugi muszą być unikalne.

Zadanie 03: Profil użytkownika i przesyłanie plików

Lokalizacja: fastapi-assignments/03-user-profile-uploads/

Co się nauczysz

  • Jak przyjmować przesyłane pliki za pomocą multipart form data
  • Jak serwować pliki statyczne (obrazy, CSS) przez HTTP
  • Różnica między JSON body a danymi formularza
  • Podstawowe zasady bezpieczeństwa ścieżek plików

Kluczowe pojęcia

Multipart form data to format kodowania żądań HTTP, który może przenosić jednocześnie pola tekstowe i pliki binarne. Standardowy format JSON nie obsługuje danych binarnych. Gdy przeglądarka przesyła formularz zawierający pole wyboru pliku, automatycznie stosuje kodowanie multipart.

Serwowanie plików statycznych polega na udostępnianiu plików z systemu plików serwera poprzez adresy URL HTTP. Middleware StaticFiles w FastAPI mapuje prefiks URL na lokalny katalog. Plik zapisany jako static/uploads/avatar.png staje się dostępny pod adresem http://localhost:8000/static/uploads/avatar.png.

secure_filename (z biblioteki Werkzeug, używanej pośrednio) oczyszcza nazwy plików podanych przez użytkownika, chroniąc przed atakami path traversal. Atakujący mógłby spróbować przesłać plik o nazwie ../../etc/passwd, aby nadpisać pliki systemowe. Funkcja secure_filename usuwa separatory ścieżek i inne niebezpieczne znaki.

Instalacja

pip install fastapi uvicorn[standard] python-multipart

Pakiet python-multipart jest wymagany, aby FastAPI mógł parsować dane formularzy i przesyłane pliki. Bez niego FastAPI nie jest w stanie przetworzyć żądań multipart/form-data.

Uruchamianie serwera

uvicorn main:app --reload

Serwer tworzy katalog static/uploads/, jeśli jeszcze nie istnieje.

Pliki projektu

main.py

Linia app.mount("/static", StaticFiles(directory="static"), name="static") dołącza middleware plików statycznych. Każdy plik w katalogu static/ staje się bezpośrednio dostępny do pobrania pod odpowiadającym mu adresem URL.

Endpoint upload_profile_image używa UploadFile = File(...). UploadFile to opakowanie FastAPI wokół przesłanego pliku, które udostępnia:

  • .filename — oryginalna nazwa pliku podana przez klienta
  • .content_type — typ MIME (np. image/jpeg)
  • .read() — odczyt zawartości pliku jako bajtów (asynchronicznie)

Plik jest zapisywany na dysk za pomocą shutil.copyfileobj(file.file, buffer), który strumieniuje plik z bufora przesyłania bezpośrednio do pliku wyjściowego bez wczytywania całej zawartości do pamięci naraz.

Endpoint update_profile używa bio: str = Form(None). Zapis Form(None) informuje FastAPI, że ten parametr należy odczytać z body formularza, a nie z body JSON. Domyślna wartość None sprawia, że pole jest opcjonalne.

schemas.py

W ProfileResponse pole profile_image_url: Optional[str] = None przyjmuje wartość null w odpowiedzi JSON, gdy profil nie ma przypisanego obrazu.

Testowanie krok po kroku

  1. Uruchom serwer.
  2. GET /profiles/john_doe — zwróć uwagę, że profile_image_url ma wartość null.
  3. W interfejsie Swagger UI użyj POST /profiles/john_doe/upload-image. Kliknij "Try it out", następnie wybierz dowolny plik graficzny i prześlij go.
  4. Odpowiedź zawiera profile_image_url w postaci /static/uploads/john_doe.jpg.
  5. Otwórz http://127.0.0.1:8000/static/uploads/john_doe.jpg w przeglądarce — obraz jest serwowany bezpośrednio.
  6. PATCH /profiles/john_doe z polem formularza bio=Programista webowy. Bio profilu zostanie zaktualizowane.

Częste problemy

  • 422 Unprocessable Entity przy przesyłaniu pliku — pakiet python-multipart nie jest zainstalowany.
  • FileNotFoundError — katalog static/uploads/ nie istnieje. Utwórz go ręcznie: mkdir -p static/uploads.

Zadanie 04: Menedżer zadań z zadaniami w tle

Lokalizacja: fastapi-assignments/04-task-manager-bg-tasks/

Co się nauczysz

  • Czym są zadania w tle i dlaczego mają znaczenie
  • Mechanizm BackgroundTasks w FastAPI
  • Dlaczego serwery WWW muszą odpowiadać szybko
  • Jak symulować wolne operacje wejścia/wyjścia na potrzeby testów

Kluczowe pojęcia

Cykl żądanie-odpowiedź ma fundamentalne ograniczenie: klient czeka na odpowiedź. Jeśli handler trasy wykonuje się przez 30 sekund (wysyłanie e-maila, generowanie raportu), klient czeka 30 sekund. Jest to niedopuszczalne w aplikacjach przeznaczonych dla użytkowników.

Zadania w tle przełamują to ograniczenie. Obiekt BackgroundTasks w FastAPI pozwala zaplanować wywołanie funkcji po wysłaniu odpowiedzi do klienta. Klient natychmiast otrzymuje odpowiedź, a czasochłonna praca odbywa się w tle.

Ten wzorzec nadaje się do operacji niekrytycznych, gdzie natychmiastowe potwierdzenie nie jest wymagane — wysyłanie powitalnych e-maili, zapisywanie do logów audytowych, wyzwalanie generowania raportów. W przypadku operacji krytycznych (np. przetwarzania płatności) konieczna jest właściwa kolejka zadań (Celery, RQ itd.) z mechanizmem trwałości danych i ponownych prób.

EmailStr to typ Pydantic, który waliduje adresy e-mail przy użyciu standardowego algorytmu. Odrzuca oczywiście nieprawidłowe ciągi znaków, takie jak "nie-jest-emailem", zanim żądanie dotrze do Twojego handlera.

Instalacja

pip install fastapi uvicorn[standard] "pydantic[email]"

Dodatek pydantic[email] instaluje bibliotekę email-validator, która obsługuje typ EmailStr.

Uruchamianie serwera

uvicorn main:app --reload

Pliki projektu

schemas.py

TaskCreate wymaga pól title: str oraz user_email: EmailStr. Typ EmailStr jest importowany z pydantic. Pydantic weryfikuje poprawność składniową adresu e-mail, zanim handler zostanie wywołany.

email_utils.py

Funkcja send_task_notification używa time.sleep(5) do symulowania opóźnienia związanego z połączeniem z zewnętrznym serwisem pocztowym. Faktyczna wysyłka e-maili wiąże się z sieciowymi przesyłami danych, które mogą zajmować kilka sekund. Moduł logging zapisuje komunikaty do terminala, dzięki czemu można obserwować, kiedy zadanie w tle się rozpoczyna i kończy.

main.py

Endpoint complete_task ma następującą sygnaturę:

async def complete_task(task_id: int, background_tasks: BackgroundTasks):

FastAPI wstrzykuje BackgroundTasks automatycznie — nie jest wymagane użycie Depends. Wywołanie background_tasks.add_task(send_task_notification, ...) rejestruje funkcję, ale jej jeszcze nie wywołuje. FastAPI wywoła ją dopiero po dostarczeniu odpowiedzi.

Testowanie krok po kroku

  1. Uruchom serwer z widocznym terminalem.
  2. Utwórz zadanie: POST /tasks z body {"title": "Napisz raport", "user_email": "user@example.com"}. Zanotuj zwrócone id (np. 1).
  3. Oznacz je jako ukończone: POST /tasks/1/complete.
  4. Uważnie obserwuj terminal. Swagger UI wyświetli odpowiedź natychmiast. W terminalu zobaczysz Starting background task..., a po pięciu sekundach — Notification SENT to user@example.com.

To opóźnienie demonstruje asynchroniczny charakter zadań w tle: odpowiedź HTTP została wysłana, zanim symulowana praca e-mailowa zakończyła się.


Zadanie 05: Panel pogodowy

Lokalizacja: fastapi-assignments/05-weather-dashboard/

Co się nauczysz

  • Jak wysyłać żądania HTTP z poziomu aplikacji FastAPI
  • Asynchroniczne klienty HTTP (httpx)
  • Cache w pamięci operacyjnej z TTL (time-to-live)
  • Walidacja parametrów zapytania (query parameters)

Kluczowe pojęcia

Integracja z zewnętrznym API oznacza, że Twoja aplikacja pełni rolę klienta wobec usługi trzeciej. Twój serwer odbiera żądanie, przekazuje żądanie do zewnętrznego API, przetwarza odpowiedź i zwraca wynik pierwotnemu klientowi. Łańcuch jest następujący: przeglądarka → Twoja aplikacja FastAPI → API Open-Meteo.

httpx to asynchroniczny klient HTTP dla Pythona, podobny do popularnej biblioteki requests, ale z natywną obsługą async/await. Gdy handler wywołuje await client.get(url), Python może obsługiwać inne żądania w czasie oczekiwania na zewnętrzną odpowiedź — serwer nigdy nie jest blokowany.

Cache przechowuje wynik kosztownej operacji, aby przyszłe żądania mogły z niego skorzystać bez jej powtarzania. Tu "kosztownym" elementem jest wychodzące wywołanie HTTP do Open-Meteo. Odpowiedzi z cache są zwracane natychmiast; rzeczywiste wywołania API są wykonywane tylko wtedy, gdy cache jest pusty lub dane są zbyt stare.

TTL (time-to-live) określa, jak długo wpis w cache pozostaje ważny. Dane pogodowe zmieniają się co godzinę; TTL wynoszące 15 minut oznacza, że cache jest odświeżany co najwyżej cztery razy na godzinę dla danej pary współrzędnych. Po upływie TTL następne żądanie wyzwala nowe wywołanie API.

Instalacja

pip install fastapi uvicorn[standard] httpx

Uruchamianie serwera

uvicorn main:app --reload

Aplikacja wymaga połączenia z internetem, ponieważ wywołuje API Open-Meteo.

Pliki projektu

weather_service.py

Metoda WeatherService.get_weather używa async with httpx.AsyncClient() as client do utworzenia zarządzanego klienta HTTP. Instrukcja async with gwarantuje prawidłowe zamknięcie połączenia po wyjściu z bloku — nawet w przypadku wyjątku.

Endpoint API Open-Meteo https://api.open-meteo.com/v1/forecast przyjmuje szerokość geograficzną, długość geograficzną i listę zmiennych. Struktura odpowiedzi JSON jest następująca:

{
  "current_weather": {
    "temperature": 15.2,
    ...
  }
}

schemas.py

WeatherRequest waliduje parametry zapytania:

  • latitude: float = Query(ge=-90, le=90) — wartość musi być między -90 a 90
  • longitude: float = Query(ge=-180, le=180) — wartość musi być między -180 a 180

Pole city_name jest opcjonalne (Optional[str] = None). API samo w sobie nie wyszukuje miast po nazwie — potrzebuje współrzędnych. Nazwa miasta jest akceptowana wyłącznie w celach informacyjnych i jest uwzględniana w odpowiedzi.

WeatherResponse zawiera pole cached: bool. Gdy odpowiedź pochodzi z cache, przyjmuje wartość True; gdy pochodzi z rzeczywistego API — False. Ta flaga pozwala zweryfikować, czy mechanizm cache działa poprawnie.

main.py

Cache to słownik: weather_cache: dict = {}. Kluczem jest krotka zaokrąglonych współrzędnych (round(lat, 1), round(lon, 1)). Zaokrąglanie zapobiega chybieniom cache spowodowanym trywialnymi różnicami, jak 51.5001 kontra 51.5002.

Każdy wpis w cache to słownik z kluczami data (odpowiedź pogodowa) i timestamp (czas pobrania). Sprawdzenie TTL wygląda następująco:

if time.time() - entry["timestamp"] < 900:  # 900 sekund = 15 minut
    return cached data

Testowanie krok po kroku

  1. Uruchom serwer.
  2. GET /weather?city_name=Warsaw&latitude=52.2&longitude=21.0
  3. Zanotuj "cached": false w odpowiedzi.
  4. Powtórz dokładnie to samo żądanie natychmiast.
  5. Zanotuj "cached": true — odpowiedź pochodzi z cache i była niemal natychmiastowa.
  6. Odczekaj 15 minut (lub tymczasowo zmniejsz TTL do 10 sekund na potrzeby testów) i powtórz. Ponownie pojawi się "cached": false.

Zadanie 06: Czat w czasie rzeczywistym

Lokalizacja: fastapi-assignments/06-real-time-chat/

Co się nauczysz

  • Protokół WebSocket i czym różni się od HTTP
  • Zarządzanie połączeniami wielu klientów
  • Rozsyłanie wiadomości do wszystkich podłączonych klientów (broadcasting)
  • Obsługa rozłączeń w sposób kontrolowany

Kluczowe pojęcia

HTTP a WebSockets: HTTP to protokół żądanie-odpowiedź — klient wysyła żądanie, serwer odsyła odpowiedź, a połączenie jest zamykane (lub utrzymywane bezczynnie do ponownego użycia). WebSockets ustanawiają trwały, dwukierunkowy kanał komunikacji. Po otwarciu połączenia obie strony mogą wysyłać wiadomości w dowolnym momencie bez konieczności nowego żądania.

Handshake WebSocket rozpoczyna się jako żądanie HTTP ze specjalnymi nagłówkami (Upgrade: websocket). Serwer odpowiada statusem 101 Switching Protocols, a od tego momentu połączenie TCP przenosi ramki WebSocket zamiast HTTP.

Broadcasting to wysyłanie wiadomości do wielu odbiorców. Gdy klient wysyła wiadomość czatu, serwer musi ją przekazać wszystkim pozostałym podłączonym klientom. Wymaga to od serwera prowadzenia listy wszystkich aktywnych połączeń.

Rozłączenie klienta może nastąpić na kilka sposobów: użytkownik zamknie kartę, sieć zostanie przerwana lub przeglądarka zostanie zamknięta. FastAPI zgłasza wyjątek WebSocketDisconnect w takim przypadku. Przechwycenie tego wyjątku pozwala serwerowi usunąć rozłączonego klienta z listy.

Instalacja

pip install fastapi uvicorn[standard]

Żadne dodatkowe pakiety nie są wymagane — FastAPI natywnie obsługuje WebSockets.

Uruchamianie serwera

uvicorn main:app --reload

Pliki projektu

chat_manager.py

Klasa ConnectionManager przechowuje słownik active_connections: dict[int, WebSocket]. Kluczem jest ID klienta (losowa liczba całkowita przypisana przy połączeniu), a wartością obiekt WebSocket.

Cztery metody:

  • connect(client_id, websocket) — wywołuje await websocket.accept(), aby zakończyć handshake WebSocket, a następnie dodaje połączenie do słownika.
  • disconnect(client_id) — usuwa połączenie ze słownika.
  • send_personal_message(message, client_id) — wysyła wiadomość do konkretnego klienta.
  • broadcast(message, sender_id) — iteruje po wszystkich połączeniach i wysyła wiadomość do wszystkich poza nadawcą.

main.py

Trasa główna (GET /) zwraca HTMLResponse zawierający kompletną stronę HTML z osadzonym JavaScriptem. Strona tworzy obiekt WebSocket wskazujący na ws://localhost:8000/ws/{clientId} i rejestruje obsługę zdarzeń: onmessage (wyświetlanie przychodzących wiadomości), onclose (wyświetlenie komunikatu o rozłączeniu).

Endpoint WebSocket:

@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(client_id, websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(f"Client {client_id}: {data}", client_id)
    except WebSocketDisconnect:
        manager.disconnect(client_id)
        await manager.broadcast(f"Client {client_id} left the chat", client_id)

Pętla while True utrzymuje połączenie aktywne, oczekując na wiadomości. Funkcja receive_text() blokuje (asynchronicznie) do momentu nadejścia wiadomości.

Testowanie krok po kroku

  1. Uruchom serwer.
  2. Otwórz http://127.0.0.1:8000/ w jednej karcie przeglądarki.
  3. Otwórz ten sam adres w drugiej karcie.
  4. Każda karta wyświetla inny ID klienta.
  5. Wpisz wiadomość w karcie 1 i kliknij "Send". Wiadomość pojawi się w karcie 2 (i w karcie 1).
  6. Zamknij kartę 1. W karcie 2 pojawi się komunikat "Client X left the chat".

Zadanie 07: Obsługa formularzy i szablony Jinja2

Lokalizacja: fastapi-assignments/07-forms-jinja2/

Co się nauczysz

  • Renderowanie HTML po stronie serwera za pomocą Jinja2
  • Przetwarzanie przesłanych formularzy HTML
  • Walidacja po stronie serwera z komunikatami błędów dla użytkownika
  • Wzorzec GET/POST dla formularzy
  • Wstępne wypełnianie pól formularza po nieudanej walidacji

Kluczowe pojęcia

Renderowanie po stronie serwera (SSR) oznacza, że serwer generuje kompletne strony HTML i wysyła je do przeglądarki. Przeglądarka wyświetla HTML bez uruchamiania żadnego frontendowego frameworka JavaScript. To tradycyjne podejście stosowane przez Django, Rails i klasyczne aplikacje PHP.

Jinja2 to silnik szablonów. Szablon to plik HTML ze specjalnymi znacznikami ({{ zmienna }}) i strukturami sterującymi ({% if warunek %}, {% for element in lista %}). Serwer wypełnia znaczniki danymi i wysyła wynikowy HTML do klienta.

Wzorzec POST/Redirect/GET: po przesłaniu formularza, który modyfikuje stan (tworzenie rekordu), należy przekierować do endpointu GET zamiast renderować odpowiedź bezpośrednio. Zapobiega to ponownemu przesłaniu formularza przez przeglądarkę, gdy użytkownik naciśnie przycisk Wstecz lub odświeży stronę. Zadanie używa uproszczonej wersji — renderuje stronę sukcesu bezpośrednio — ale rzeczywista aplikacja powinna stosować RedirectResponse.

Instalacja

pip install fastapi uvicorn[standard] jinja2 python-multipart

Jinja2 to domyślny silnik szablonów FastAPI. python-multipart jest wymagany do parsowania przesłanych formularzy.

Uruchamianie serwera

uvicorn main:app --reload

Pliki projektu

main.py

Jinja2Templates(directory="templates") tworzy renderer szablonów wskazujący na katalog templates/. Ścieżka katalogu jest względna do miejsca, z którego uruchamiasz uvicorn.

Trasa get_subscription_form zwraca TemplateResponse. Słownik kontekstu musi zawierać {"request": request} — jest to wymóg Jinja2/FastAPI. Dodatkowe zmienne (komunikaty błędów, poprzednio przesłane wartości) są również przekazywane w kontekście.

Trasa handle_subscription:

  1. Odczytuje pola formularza: name: str = Form(...) i email: str = Form(...).
  2. Ręcznie je waliduje (sprawdzenie długości dla imienia, regex dla e-maila).
  3. W razie błędu walidacji: ponownie renderuje index.html z oryginalnymi wartościami i komunikatami błędów, dzięki czemu użytkownik nie musi ponownie wpisywać wszystkiego.
  4. W razie powodzenia walidacji: dołącza dane do bazy i renderuje success.html.

templates/index.html

Składnia warunkowa Jinja2: {% if error_name %}<p class="error">{{ error_name }}</p>{% endif %}.

Wstępne wypełnianie używa: value="{{ name if name else '' }}". Jeśli name zostało przekazane w kontekście, wypełnia pole input; w przeciwnym razie pole jest puste.

templates/success.html

Prosta strona renderująca {{ name }} i {{ email }} z kontekstu.

Testowanie krok po kroku

  1. Uruchom serwer. Otwórz http://127.0.0.1:8000/.
  2. Prześlij formularz z poprawnym imieniem i adresem e-mail. Powinna pojawić się strona sukcesu.
  3. Wróć wstecz. Prześlij formularz z imieniem złożonym z jednego znaku (np. "A"). Formularz zostanie ponownie wyrenderowany z komunikatem błędu, a pole e-mail zachowa poprzednio wpisaną wartość.
  4. Prześlij formularz z nieprawidłowym adresem e-mail (np. "nie-jest-emailem"). Formularz zostanie ponownie wyrenderowany z błędem e-mail, a pole imienia zachowa swoją wartość.

Zadanie 08: Odbiorca webhooków

Lokalizacja: fastapi-assignments/08-webhook-receiver/

Co się nauczysz

  • Czym są webhooks i jak są stosowane w praktyce
  • Weryfikacja podpisu HMAC
  • Dlaczego do weryfikacji podpisu należy używać surowego body żądania
  • Porównywanie stałoczasowe i ochrona przed atakami czasowymi

Kluczowe pojęcia

Webhooks to wywołania zwrotne HTTP (HTTP callbacks). Zamiast odpytywać zewnętrzny serwis w poszukiwaniu aktualizacji, zewnętrzny serwis sam wysyła powiadomienia do Twojej aplikacji poprzez żądania HTTP POST. GitHub wysyła zdarzenia webhook przy każdym pushu kodu; Stripe wysyła je, gdy płatność zakończy się powodzeniem. Twoja aplikacja musi udostępniać publicznie dostępny URL, pod który będą kierowane te żądania.

HMAC (Hash-based Message Authentication Code) to mechanizm służący do weryfikacji zarówno integralności, jak i autentyczności wiadomości. Nadawca i odbiorca dzielą tajny klucz. Nadawca oblicza HMAC(klucz, wiadomość) i dołącza wynik do żądania. Odbiorca niezależnie oblicza HMAC(klucz, odebrana_wiadomość) i porównuje z dostarczonym podpisem. Jeśli są zgodne, wiadomość nie została zmodyfikowana i pochodzi od kogoś, kto zna klucz.

Wymóg surowego body: parsery JSON nie zachowują nieistotnych białych znaków ani kolejności kluczy. Dwie reprezentacje JSON tych samych danych mogą dawać różne sekwencje bajtów. HMAC jest obliczane na bajtach, nie na sparsowanych obiektach. Dlatego podpis musi być weryfikowany względem dokładnej sekwencji bajtów otrzymanej w żądaniu, a nie względem ponownie zserializowanego słownika Pythona.

Ataki czasowe: naiwne porównanie ciągów znaków (a == b) zwraca False natychmiast po znalezieniu pierwszego różniącego się znaku. Oznacza to, że porównania nieprawidłowych podpisów kończą się nieco szybciej niż porównania podpisów niemal prawidłowych. Atakujący może mierzyć te różnice czasowe i na ich podstawie odgadywać prawidłowy podpis znak po znaku. Funkcja hmac.compare_digest wykonuje się przez taki sam czas niezależnie od miejsca rozbieżności ciągów znaków, eliminując ten wektor ataku.

Instalacja

pip install fastapi uvicorn[standard]

Wszystkie wymagane funkcje kryptograficzne (hmac, hashlib) są częścią biblioteki standardowej Pythona.

Uruchamianie serwera

uvicorn main:app --reload

Pliki projektu

webhook_utils.py

def verify_signature(payload: bytes, secret: bytes, signature: str) -> bool:
    expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

hmac.new tworzy obiekt HMAC używając sha256 jako algorytmu skrótu. Metoda .hexdigest() zwraca wynik jako ciąg znaków szesnastkowych zapisanych małymi literami.

main.py

body = await request.body()

Ta linia odczytuje surowe bajty przed jakimkolwiek parsowaniem JSON. Weryfikacja podpisu jest przeprowadzana na body. Dopiero po pomyślnej weryfikacji body jest parsowane:

payload = json.loads(body)

Endpoint zwraca 403 Forbidden (w niektórych implementacjach 401 Unauthorized), gdy podpis jest brakujący lub nieprawidłowy, nie logując szczegółów dotyczących przesłanego payload, aby uniknąć wycieku informacji.

Testowanie krok po kroku

Użyj skryptu Pythona z pliku README, aby wysłać poprawnie podpisane żądanie. Następnie zmień wartość secret w skrypcie na błędną i zaobserwuj odpowiedź 401. Potwierdza to, że niepodpisane lub błędnie podpisane żądania są odrzucane.


Zadanie 09: System zarządzania magazynem

Lokalizacja: fastapi-assignments/09-inventory-management/

Co się nauczysz

  • Strategie wersjonowania API i ich kompromisy
  • Paginacja z użyciem offset i limit
  • Różnica między ustrukturyzowanymi i nieustrukturyzowanymi odpowiedziami paginacji
  • Organizacja aplikacji FastAPI za pomocą APIRouter

Kluczowe pojęcia

Wersjonowanie API pozwala serwerowi rozwijać API bez naruszania działania istniejących klientów. Podejście z prefiksem URL (/v1, /v2) czyni wersję jawną i widoczną. Gdy V2 wprowadza zmiany niekompatybilne wstecz (inny kształt odpowiedzi, usunięte pola), V1 pozostaje dostępne dla klientów, którzy jeszcze nie dokonali migracji.

Paginacja jest niezbędna, gdy kolekcja zasobów jest zbyt duża, aby zwrócić ją w jednej odpowiedzi. Zamiast zwracać wszystkie elementy, API przyjmuje parametry offset (ile elementów pominąć) i limit (ile elementów zwrócić). Klient pobiera pierwszą stronę z offset=0&limit=20, drugą z offset=20&limit=20 i tak dalej.

has_more to flaga w odpowiedzi V2, która informuje klienta, czy istnieją kolejne strony. Jest obliczana jako offset + limit < total. Bez tej flagi klient musiałby wysyłać dodatkowe żądanie, aby sprawdzić, czy osiągnął ostatnią stronę.

APIRouter porządkuje powiązane trasy w grupy. Każdy router może mieć własny prefix, tagi i middleware. Główna aplikacja następnie dołącza routery: app.include_router(v1_router). Mechanizm ten jest analogiczny do Blueprints we Flasku.

Instalacja

pip install fastapi uvicorn[standard]

Uruchamianie serwera

uvicorn main:app --reload

Pliki projektu

schemas.py

PaginatedItems to model odpowiedzi dla V2:

class PaginatedItems(BaseModel):
    items: list[ItemResponse]
    total: int
    offset: int
    limit: int
    has_more: bool

V1 zwraca bezpośrednio list[ItemResponse] — prostszą strukturę pozbawioną metadanych.

v1/router.py

router = APIRouter(prefix="/v1", tags=["Inventory V1"])

Endpoint zwraca wycinek listy: inventory_data[offset : offset + limit]. Brak metadanych — wyłącznie surowa lista.

v2/router.py

router = APIRouter(prefix="/v2", tags=["Inventory V2"])

Endpoint filtruje według kategorii (jeśli podana), oblicza total, wylicza has_more i zwraca obiekt PaginatedItems.

main.py

app.include_router(v1_router)
app.include_router(v2_router)

Oba routery są zarejestrowane w tej samej aplikacji. Dokumentacja dostępna pod /docs wyświetla wszystkie trasy pogrupowane według tagów.

Testowanie krok po kroku

  1. GET /v1/items?offset=0&limit=5 — zwraca 5 elementów jako tablicę JSON.
  2. GET /v1/items?offset=5&limit=5 — zwraca kolejne 5 elementów.
  3. GET /v2/items?offset=0&limit=5 — zwraca ustrukturyzowany obiekt z polami items, total, has_more.
  4. GET /v2/items?category=Electronics&offset=0&limit=10 — wyniki przefiltrowane według kategorii.
  5. Zwróć uwagę na różnicę w strukturze odpowiedzi między V1 a V2.

Zadanie 10: Bezpieczny sejf dokumentów

Lokalizacja: fastapi-assignments/10-secure-document-vault/

Co się nauczysz

  • Uwierzytelnianie JWT (JSON Web Token)
  • Hashowanie haseł za pomocą bcrypt
  • Przepływy OAuth2 w FastAPI
  • Autoryzacja oparta na zakresach (scope-based authorisation)
  • Kontrola dostępu oparta na rolach (RBAC) przez poziomy poświadczeń

Kluczowe pojęcia

Uwierzytelnianie odpowiada na pytanie "kim jesteś?" — weryfikuje tożsamość użytkownika. Autoryzacja odpowiada na pytanie "co możesz robić?" — sprawdza, czy zidentyfikowany użytkownik ma uprawnienia do konkretnej operacji. To odrębne koncepcje, które muszą być zaimplementowane poprawnie niezależnie od siebie.

Hasła nigdy nie mogą być przechowywane w postaci jawnej. Hash hasła to transformacja jednokierunkowa: dla danego hasła oblicza się hash(hasła). Weryfikacja polega na zahashowaniu wejścia i porównaniu z przechowywanym hashem. Nawet jeśli baza danych zostanie skompromitowana, atakujący nie może odzyskać oryginalnych haseł z hashy. Bcrypt to celowo wolny algorytm hashowania zaprojektowany z myślą o hasłach. Jego powolność sprawia, że ataki brute-force są niepraktyczne.

JWT (JSON Web Token) to kompaktowy, bezpieczny dla URL format tokenu. Składa się z trzech części kodowanych Base64, oddzielonych kropkami: nagłówka (algorytm), payload (twierdzenia) i podpisu. Serwer wystawia JWT w momencie uwierzytelnienia użytkownika. Kolejne żądania zawierają JWT w nagłówku Authorization: Bearer <token>. Serwer weryfikuje podpis bez konsultacji z bazą danych, co czyni JWT bezstanowymi.

Zakresy OAuth2 (OAuth2 Scopes) to granularne uprawnienia osadzone w JWT. Token może zawierać zakresy ["vault:read", "vault:write"]. Endpoint wymagający vault:write odrzuci tokeny zawierające wyłącznie vault:read. Pozwala to na wystawianie tokenów z minimalnymi wymaganymi uprawnieniami.

Poziomy poświadczeń implementują kontrolę dostępu na poziomie obiektu. Użytkownik z poziomem poświadczeń 3 nie może odczytać dokumentu sklasyfikowanego na poziomie 4, nawet jeśli posiada zakres vault:read. Jest to egzekwowane w handlerze trasy poprzez porównanie current_user["clearance_level"] z document["secret_level"].

Instalacja

pip install fastapi uvicorn[standard] python-jose[cryptography] passlib[bcrypt] python-multipart
  • python-jose — kodowanie i dekodowanie JWT
  • passlib[bcrypt] — hashowanie haseł
  • python-multipart — wymagany do przesyłania formularzy OAuth2

Uruchamianie serwera

uvicorn main:app --reload

Pliki projektu

auth.py

OAuth2PasswordBearer z parametrem scopes informuje interfejs dokumentacji FastAPI, aby wyrenderował okno dialogowe autoryzacji, w którym użytkownicy mogą wybrać żądane zakresy.

pwd_context = CryptContext(schemes=["bcrypt"]) tworzy kontekst Passlib. Metoda pwd_context.verify(plain, hashed) weryfikuje hasło; pwd_context.hash(password) tworzy nowy hash.

Funkcja create_access_token buduje payload JWT i podpisuje go za pomocą jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM). Payload zawiera sub (podmiot, czyli nazwę użytkownika) oraz scopes.

Funkcja get_current_user to zależność FastAPI, która:

  1. Automatycznie odbiera token bearer z nagłówka żądania (poprzez OAuth2PasswordBearer)
  2. Dekoduje JWT za pomocą jwt.decode
  3. Wyszukuje użytkownika w mockowej bazie danych
  4. Sprawdza, czy token zawiera wszystkie zakresy wymagane przez wywołujący endpoint (przekazane przez security_scopes.scopes)

main.py

Endpoint /token używa OAuth2PasswordRequestForm — wbudowanego elementu FastAPI, który odczytuje username i password z body formularza. Po uwierzytelnieniu wystawia JWT z zakresami użytkownika.

Chronione endpointy deklarują swoje wymagania:

@app.get("/documents")
async def get_documents(current_user=Security(get_current_user, scopes=["vault:read"])):

Security działa jak Depends, ale przenosi informacje o zakresach. FastAPI przekazuje wymagane zakresy do get_current_user poprzez security_scopes.

Testowanie krok po kroku

  1. Otwórz http://127.0.0.1:8000/docs.
  2. Kliknij "Authorize" (prawy górny róg). Wprowadź nazwę użytkownika alice, hasło alice123 i wybierz zakresy vault:read vault:write. Kliknij "Authorize".
  3. GET /documents — wyświetlone zostaną dokumenty do poziomu poświadczeń 3.
  4. POST /documents z body {"title": "Tajne", "content": "...", "secret_level": 4} — otrzymasz odpowiedź 403 Forbidden (poziom poświadczeń Alice wynosi 3, co jest poniżej wymaganego 4).
  5. POST /documents z secret_level: 2 — operacja zakończy się powodzeniem.
  6. GET /admin/users — nie powiedzie się dla Alice (brak zakresu admin).
  7. Wyloguj się, zaloguj jako admin z hasłem admin123. Teraz GET /admin/users zakończy się powodzeniem.