Table of Contents
- FastAPI — Przewodnik po zadaniach
- Wymagania wstępne
- Zadanie 01: Proste API do zarządzania zadaniami (To-Do)
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Częste problemy
- Zadanie 02: Osobisty blog
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 03: Profil użytkownika i przesyłanie plików
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Częste problemy
- Zadanie 04: Menedżer zadań z zadaniami w tle
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 05: Panel pogodowy
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 06: Czat w czasie rzeczywistym
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 07: Obsługa formularzy i szablony Jinja2
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 08: Odbiorca webhooków
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 09: System zarządzania magazynem
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 10: Bezpieczny sejf dokumentów
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 poletitle(niepusty ciąg znaków) oraz opcjonalnedescription.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 oid(nadawane przez serwer) icompleted(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ę:
- Odbiera zwalidowane dane żądania przekazane przez FastAPI
- Operuje na liście w pamięci
- 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
- Uruchom serwer:
uvicorn main:app --reload - Otwórz
http://127.0.0.1:8000/docs - Kliknij
POST /todos, następnie "Try it out". Wprowadź body w postaci{"title": "Kupić zakupy"}i kliknij "Execute". Zanotujidw odpowiedzi. - Kliknij
GET /todosi wykonaj żądanie. Powinieneś zobaczyć swoje zadanie na liście. - Kliknij
PUT /todos/{todo_id}. Wprowadźidz kroku 3 i body w postaci{"title": "Kupić zakupy i mleko"}. - Kliknij
DELETE /todos/{todo_id}z tym samymid. - 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:
engine— połączenie z plikiem SQLite. Ciągsqlite:///./blog.dboznacza "SQLite, ścieżka względna, plik o nazwie blog.db".SessionLocal— fabryka tworząca sesje bazy danych. Każde żądanie HTTP otrzymuje własną sesję.get_db— funkcja generatorowa używana jako zależność FastAPI. Bloktry/finallygwarantuje 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
- Uruchom serwer.
- Utwórz wpis:
POST /postsz body{"title": "Witaj świecie", "slug": "witaj-swiecie", "content": "Mój pierwszy wpis."}. - Pobierz go:
GET /posts/witaj-swiecie. - Zatrzymaj i uruchom ponownie serwer. Następnie pobierz wpis jeszcze raz. Nadal tam jest — w odróżnieniu od zadania 01, dane zostały utrwalone.
- 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
- Uruchom serwer.
GET /profiles/john_doe— zwróć uwagę, żeprofile_image_urlma wartośćnull.- 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. - Odpowiedź zawiera
profile_image_urlw postaci/static/uploads/john_doe.jpg. - Otwórz
http://127.0.0.1:8000/static/uploads/john_doe.jpgw przeglądarce — obraz jest serwowany bezpośrednio. PATCH /profiles/john_doez polem formularzabio=Programista webowy. Bio profilu zostanie zaktualizowane.
Częste problemy
422 Unprocessable Entityprzy przesyłaniu pliku — pakietpython-multipartnie jest zainstalowany.FileNotFoundError— katalogstatic/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
BackgroundTasksw 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
- Uruchom serwer z widocznym terminalem.
- Utwórz zadanie:
POST /tasksz body{"title": "Napisz raport", "user_email": "user@example.com"}. Zanotuj zwróconeid(np.1). - Oznacz je jako ukończone:
POST /tasks/1/complete. - 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 90longitude: 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
- Uruchom serwer.
GET /weather?city_name=Warsaw&latitude=52.2&longitude=21.0- Zanotuj
"cached": falsew odpowiedzi. - Powtórz dokładnie to samo żądanie natychmiast.
- Zanotuj
"cached": true— odpowiedź pochodzi z cache i była niemal natychmiastowa. - 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łujeawait 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
- Uruchom serwer.
- Otwórz
http://127.0.0.1:8000/w jednej karcie przeglądarki. - Otwórz ten sam adres w drugiej karcie.
- Każda karta wyświetla inny ID klienta.
- Wpisz wiadomość w karcie 1 i kliknij "Send". Wiadomość pojawi się w karcie 2 (i w karcie 1).
- 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:
- Odczytuje pola formularza:
name: str = Form(...)iemail: str = Form(...). - Ręcznie je waliduje (sprawdzenie długości dla imienia, regex dla e-maila).
- W razie błędu walidacji: ponownie renderuje
index.htmlz oryginalnymi wartościami i komunikatami błędów, dzięki czemu użytkownik nie musi ponownie wpisywać wszystkiego. - 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
- Uruchom serwer. Otwórz
http://127.0.0.1:8000/. - Prześlij formularz z poprawnym imieniem i adresem e-mail. Powinna pojawić się strona sukcesu.
- 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ść.
- 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
GET /v1/items?offset=0&limit=5— zwraca 5 elementów jako tablicę JSON.GET /v1/items?offset=5&limit=5— zwraca kolejne 5 elementów.GET /v2/items?offset=0&limit=5— zwraca ustrukturyzowany obiekt z polamiitems,total,has_more.GET /v2/items?category=Electronics&offset=0&limit=10— wyniki przefiltrowane według kategorii.- 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 JWTpasslib[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:
- Automatycznie odbiera token bearer z nagłówka żądania (poprzez
OAuth2PasswordBearer) - Dekoduje JWT za pomocą
jwt.decode - Wyszukuje użytkownika w mockowej bazie danych
- 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
- Otwórz
http://127.0.0.1:8000/docs. - Kliknij "Authorize" (prawy górny róg). Wprowadź nazwę użytkownika
alice, hasłoalice123i wybierz zakresyvault:read vault:write. Kliknij "Authorize". GET /documents— wyświetlone zostaną dokumenty do poziomu poświadczeń 3.POST /documentsz body{"title": "Tajne", "content": "...", "secret_level": 4}— otrzymasz odpowiedź403 Forbidden(poziom poświadczeń Alice wynosi 3, co jest poniżej wymaganego 4).POST /documentszsecret_level: 2— operacja zakończy się powodzeniem.GET /admin/users— nie powiedzie się dla Alice (brak zakresuadmin).- Wyloguj się, zaloguj jako
adminz hasłemadmin123. TerazGET /admin/userszakończy się powodzeniem.