fastapi:~/app$ cat fastapi-htmx.md

FastAPI + HTMX: pełny stos bez procesu budowania

# Twórz produkcyjne aplikacje webowe bez Reacta i webpacka: FastAPI, HTMX, Alpine.js, Jinja2, zwykły CSS, wzorce Bootstrap, i18n, wdrażanie, SEO i wydajność.

author: words: 11679 read_time: 45m updated: 2026-08-17 02:33
$ less fastapi-htmx.md

TL;DR: FastAPI + HTMX + Alpine.js + Jinja2 + zwykły CSS pozwala tworzyć produkcyjne aplikacje webowe bez narzędzi budowania, bez node_modules/ i z idealnymi wynikami Lighthouse. Ten przewodnik omawia cały system, od architektury po wdrożenie, wykorzystując blakecrosley.com jako produkcyjny przykład referencyjny, który obsługuje 210 wpisów blogowych, interaktywne komponenty JavaScript, 11 głównych przewodników, 48 studiów projektowych oraz język angielski i 9 przetłumaczonych lokalizacji bez ani jednego bundlera, kompilatora czy transpiler.1

Współczesny stos tworzenia aplikacji webowych zakłada, że potrzebne są React, webpack, TypeScript i potok budowania. Dla dużej kategorii aplikacji — serwisów opartych na treści, narzędzi wewnętrznych, aplikacji CRUD, stron portfolio, platform dokumentacyjnych — to założenie jest błędne. Stos opisany w tym przewodniku eliminuje cały frontendowy łańcuch narzędzi budowania, a jednocześnie pozwala tworzyć strony osiągające w Lighthouse wynik 100/100/100/100.2

To nie jest agitacja. To pomiar. Opisana tutaj architektura działa produkcyjnie, obsługuje rzeczywistych użytkowników w dziesięciu językach, a liczby można zweryfikować.


Kluczowe wnioski

  • Renderowany po stronie serwera HTML eliminuje trzy całe kategorie problemów: zarządzanie stanem klienta, granice serializacji JSON oraz niezgodności hydratacji. HTMX sprawia, że odpowiedzi serwera stają się ostatecznym wynikiem — bez kroku renderowania po stronie klienta.
  • Brak narzędzi do budowania oznacza brak błędów budowania. Żadnych konfliktów zależności peer przy npm install, żadnych błędów kompilatora TypeScript w plikach, których się nie tknęło, żadnych PR-ów Dependabota dla zależności tranzytywnych, których nigdy się nie importowało. Pipeline wdrożeniowy to git push.
  • Alpine.js obsługuje stan wyłącznie kliencki, którego HTMX obsłużyć nie potrafi. Rozwijane menu, modale, przełączniki nawigacji mobilnej oraz wszelki stan UI istniejący wyłącznie w przeglądarce należą do Alpine.js. Granica jest jasna: jeśli stan wymaga serwera, należy użyć HTMX. Jeśli nie wymaga, należy użyć Alpine.js.
  • Zwykły CSS z właściwościami niestandardowymi zastępuje Sass i Tailwind. Właściwości niestandardowe CSS kaskadują, dziedziczą i reagują na zapytania medialne w czasie wykonywania. Zmienne preprocesora kompilują się do statycznych wartości i znikają. Przeglądarka odczytuje właściwości niestandardowe bezpośrednio — bez kroku kompilacji.
  • Podejście to ma jasne granice. Jest błędne dla dużych zespołów współdzielących interfejsy komponentów, produktów SaaS ze złożonym stanem po stronie klienta oraz aplikacji zależnych od bibliotek z ekosystemu npm. Framework decyzyjny w sekcji 15 precyzyjnie identyfikuje tę granicę.
  • blakecrosley.com jest dowodem. Podstawowe wzorce z tego przewodnika (HTMX, Alpine.js, Jinja2, zwykły CSS) działają w środowisku produkcyjnym na blakecrosley.com. Sekcje dotyczące Bootstrap i SQLAlchemy obejmują standardowe wzorce dla stosu, które nie są używane na tej konkretnej witrynie. Każde stwierdzenie ma ścieżkę pliku, blok konfiguracyjny lub audyt Lighthouse, który można samodzielnie zweryfikować na PageSpeed Insights.2

Jak korzystać z tego przewodnika

To kompleksowe źródło informacji. Warto rozpocząć od miejsca pasującego do poziomu doświadczenia:

Doświadczenie Zacznij tutaj Następnie zgłęb
Programista Python, nowy w HTMX Teza no-buildPrzegląd architekturyDogłębna analiza HTMX Wzorce Alpine.js, Bezpieczeństwo
Programista React/Vue oceniający alternatywy Teza no-buildFramework decyzyjny Przegląd architektury, Wydajność
Programista FastAPI dodający interaktywność Dogłębna analiza HTMXWzorce Alpine.js i18n i lokalizacja, Wdrażanie
Programista full-stack budujący od podstaw Czytanie sekwencyjne od Przegląd architektury Karta szybkiego odniesienia do bieżącego użytku

Można użyć Ctrl+F / Cmd+F do wyszukania konkretnych wzorców lub atrybutów. Karta szybkiego odniesienia na końcu zawiera zwięzłe podsumowanie do przeglądania.


Teza no-build

Teza jest wąska i precyzyjna: dla witryn opartych na treści, prowadzonych przez pojedynczego programistę lub mały zespół, narzędzia do budowania rozwiązują problemy, których się nie ma, jednocześnie tworząc te, które się ma.

Oto rzeczywiste metryki z blakecrosley.com:

Metryka blakecrosley.com (no-build) Typowy projekt Next.js3
Zależności 17 pakietów Python 311+ pakietów npm
Pliki konfiguracyjne budowania 0 5-8 (next.config, tsconfig, postcss, tailwind itd.)
Rozmiar node_modules/ Nie istnieje 187 MB bazowo, 250-400 MB z dodatkami
Czas instalacji pip install: 8 sekund npm install: 30-90 sekund
Krok budowania Brak next build: 15-60 sekund
Pipeline wdrożeniowy git push → na żywo w ~40 sekund Instalacja → budowanie → wdrożenie: 2-5 minut
Wydajność Lighthouse 100 70-90 bez jawnej optymalizacji4

17 pakietów Python obejmuje FastAPI, Jinja2, Pydantic, uvicorn, nh3 i 12 innych. Żaden nie jest narzędziem do budowania. Żaden nie jest kompilatorem. Żaden nie jest bundlerem.5

Z czego się rezygnuje

Uczciwość wymaga wyliczenia rzeczywistych kosztów:

Brak TypeScript. Każdy plik .js to czysty JavaScript. Błędy typów wychwytywane są przez testy i analizę kodu, a nie przez kompilator. Sprawdza się to dla pojedynczego programisty. Nie sprawdziłoby się dla zespołu 10 osób współdzielących interfejsy komponentów.

Brak Hot Module Replacement. Zmiany w CSS wymagają ręcznego odświeżenia przeglądarki. hx-boost z HTMX sprawia, że nawigacja jest na tyle szybka, by pełne odświeżenia były tolerowalne, jednak przy intensywnych cyklach iteracji wizualnych HMR oszczędza czas.

Brak Tree Shaking. Każdy bajt JavaScript, który zostanie napisany, trafia do przeglądarki. Ograniczenie to wymusza dyscyplinę: małe, skupione pliki zamiast dużych modułów narzędziowych.

Brak bibliotek komponentów z npm. Żadnego Radix, żadnego shadcn/ui, żadnego Headless UI. Każdy element interaktywny jest budowany ręcznie lub korzysta z wbudowanych komponentów Bootstrap 5.

Brak tokenów systemu projektowego z npm. System projektowy żyje w niestandardowych właściwościach CSS. Nie można go zaimportować jako pakietu w innym projekcie.

Te kompromisy są akceptowalne dla witryny opartej na treści z jednym do trzech programistów. Byłyby nie do przyjęcia dla produktu SaaS z 15-osobowym zespołem inżynieryjnym. Sekcja 15 zawiera framework decyzyjny.

Co się zyskuje

Brak błędów budowania. Żaden npm install nie może zawieść z powodu konfliktów zależności peer. Żaden next build nie może zawieść z powodu błędu TypeScript w pliku, którego się nie tknęło.6

Debugowanie przez View Source. JavaScript działający w przeglądarce to ten sam JavaScript, który został napisany. Source mapy nie są wymagane.

Natychmiastowy start lokalny. uvicorn app.main:app --reload uruchamia się w mniej niż 2 sekundy.

Konkretny waterfall żądań. Pierwsza wizyta ładuje: jeden dokument HTML (~15KB po gzip), jeden plik CSS (~8KB), HTMX (~16KB, w cache), Alpine.js (~15KB, w cache) oraz interaktywny JS strony (~4-8KB). Łącznie: około 55-65KB przy pierwszej wizycie.1

Frontend odporny na przyszłość. Kod po stronie klienta używa HTML, CSS i JavaScript — standardów, które utrzymują wsteczną kompatybilność od 30 lat.7 Żadnej migracji Webpack 4 → 5, żadnego wycofywania Create React App, żadnej migracji do Next.js App Router.

Porównanie stosów

Jak stos no-build wypada w porównaniu z popularnymi alternatywami w mierzalnych wymiarach:

Wymiar FastAPI+HTMX (ten przewodnik) Next.js (React) Astro 11ty
JS wysyłany do przeglądarki 35-40KB (HTMX+Alpine+małe skrypty stron) 85-250KB+ (środowisko uruchomieniowe React) 0KB domyślnie, wyspy opt-in 0KB domyślnie
Krok budowania Brak Wymagany (webpack/turbopack) Wymagany (Vite) Wymagany (niestandardowy)
Pliki konfiguracyjne 0 5-8 (next.config, tsconfig itd.) 1-3 (astro.config, tsconfig) 1-2 (.eleventy.js)
Pipeline wdrożeniowy git push (40s) Instalacja+budowanie+wdrożenie (2-5min) Instalacja+budowanie+wdrożenie (1-3min) Instalacja+budowanie+wdrożenie (1-2min)
Interaktywność po stronie serwera Natywna (HTMX) Trasy API + fetch klienta Ograniczona (akcje formularzy) Brak (statyczne wyjście)
Zarządzanie stanem klienta Alpine.js (15KB) Stan/context/Redux w React Wyspy framework Ręczny JS
Język backendu Python JavaScript/TypeScript JavaScript/TypeScript JavaScript
Podejście do i18n Po stronie serwera (middleware) next-intl lub podobny pakiet @astrojs/i18n Ręczne
Wydajność Lighthouse 100 (zmierzone) 70-90 typowo4 95-100 typowo 95-100 typowo
Najlepsze dla Witryny treściowe, CRUD, dashboardy Złożone SPA, duże zespoły Witryny treściowe, marketing Statyczne blogi, dokumentacja

Astro i 11ty to najbliżsi konkurenci dla witryn treściowych. Oba produkują doskonałe statyczne wyjście, ale wymagają kroku budowania i łańcucha narzędzi JavaScript. Stos FastAPI+HTMX wymienia wydajność strony statycznej na interaktywność po stronie serwera (filtrowanie kategorii, obsługa formularzy, wyszukiwanie w czasie rzeczywistym) bez dodawania kroku budowania. Jeśli witryna jest czysto statyczna i nie ma interakcji z serwerem, Astro lub 11ty mogą okazać się lepszym wyborem.


Przegląd architektury

Przepływ żądań

Każde żądanie przechodzi przez pojedynczą ścieżkę obejmującą cztery warstwy:

Browser                FastAPI                Jinja2              HTMX/Alpine
  |                      |                     |                     |
  |--- GET /about ------>|                     |                     |
  |                      |-- render template ->|                     |
  |                      |                     |-- base.html ------->|
  |                      |                     |   + about.html      |
  |                      |<-- full HTML -------|                     |
  |<--- HTML response ---|                     |                     |
  |                                                                  |
  |--- hx-get /search ------------------------------------------------>|
  |                      |<-- HTMX request ----|                     |
  |                      |-- render partial -->|                     |
  |                      |                     |-- _results.html     |
  |                      |<-- HTML fragment ---|                     |
  |<--- HTML fragment ---|                     |                     |
  |--- DOM swap -------------------------------------------------------->|

Pełne ładowanie strony zwraca kompletne dokumenty HTML (szablon bazowy + szablon strony). Żądania HTMX zwracają fragmenty HTML (cząstkowe szablony). Serwer decyduje, co wyrenderować, na podstawie typu żądania. Alpine.js zarządza stanem po stronie klienta, który nigdy nie trafia na serwer.

Role komponentów

Komponent Rola Zakres
FastAPI Routing, logika biznesowa, dostęp do danych, walidacja Serwer
Jinja2 Renderowanie szablonów, dziedziczenie, makra Serwer
HTMX Interaktywność sterowana przez serwer (formularze, paginacja, wyszukiwanie) Klient ↔ Serwer
Alpine.js Stan wyłącznie po stronie klienta (rozwijane menu, modale, przełączniki) Tylko klient
Bootstrap 5 System siatki, klasy narzędziowe, responsywny układ Klient (CSS)
Czysty CSS Własności niestandardowe, style komponentów, tokeny projektowe Klient (CSS)
Pydantic Walidacja żądań/odpowiedzi, ustawienia Serwer

Struktura projektu

app/
├── main.py              # FastAPI app, middleware, templates
├── config.py            # Pydantic settings management
├── routes/
│   ├── pages.py         # Page routes (HTML responses)
│   └── api.py           # API routes (JSON/HTML fragment responses)
├── content.py           # Markdown loading, blog post parsing
├── security/
│   ├── headers.py       # CSP, HSTS, security headers middleware
│   ├── csrf.py          # HMAC-signed CSRF tokens
│   ├── rate_limit.py    # 3-tier rate limiting
│   └── logging.py       # Security event logging
├── i18n/
│   ├── config.py        # Supported locales, mappings
│   ├── middleware.py     # URL-based locale detection
│   ├── jinja.py         # Translation functions for templates
│   └── d1_client.py     # Cloudflare D1 translation storage
├── cache_assets.py      # Content-hash asset versioning
└── templates/
    ├── base.html         # Base layout with Alpine.js state
    ├── components/       # Reusable partials (_language_switcher.html, etc.)
    └── pages/            # Page templates (home.html, about.html, etc.)

content/
├── blog/                # Markdown blog posts with YAML frontmatter
└── guides/              # Multi-section guide markdown

static/
├── css/                 # Plain CSS (no preprocessors)
├── js/                  # Vanilla JavaScript (no bundlers)
│   └── vendor/          # Self-hosted HTMX, Alpine.js
└── images/              # Optimized images with WebP srcset

Struktura opiera się na jednej zasadzie: każdy katalog zawiera jeden typ rzeczy. Trasy znajdują się w routes/. Szablony znajdują się w templates/. Zasoby statyczne znajdują się w static/. Żaden etap budowania nie przekształca jednego w drugie.

Porównanie z architekturą SPA

W projekcie React + Next.js odpowiadająca struktura obejmowałaby:

src/
├── components/       # React components (JSX)
├── pages/            # Route handlers (also JSX)
├── api/              # API routes (also in pages/)
├── hooks/            # Custom React hooks
├── context/          # React context providers
├── lib/              # Utility functions
├── styles/           # CSS modules or Tailwind config
└── types/            # TypeScript type definitions

# Plus build configuration
next.config.js
tsconfig.json
postcss.config.js
tailwind.config.js
eslint.config.js
package.json
package-lock.json
node_modules/         # 187+ MB of dependencies

Architektura SPA wymaga koordynacji między tymi katalogami na etapie budowania. TypeScript kompiluje pliki .tsx do JavaScript. PostCSS przetwarza dyrektywy Tailwind na CSS. Webpack (lub Turbopack) łączy wynik w paczki. Każdy krok może zakończyć się niepowodzeniem niezależnie.

Architektura bez etapu budowania nie wymaga żadnej koordynacji. Szablon odwołuje się do pliku CSS. Plik CSS istnieje w static/css/. Przeglądarka ładuje go bezpośrednio. Zmiana nazwy pliku powoduje błąd odwołania w szablonie w czasie wykonania — nie w czasie budowania. To przenosi błędy z etapu kompilacji do etapu wykonania, co stanowi rzeczywisty kompromis. Dla samodzielnego programisty uruchamiającego uvicorn --reload podczas rozwoju, błędy czasu wykonania pojawiają się natychmiast w przeglądarce. W przypadku dużego zespołu błędy kompilacji wychwycone przez TypeScript zapobiegają kategorii błędów, których błędy czasu wykonania nie są w stanie wykryć.


Wzorce FastAPI

Konfiguracja aplikacji

Aplikacja jest inicjalizowana w main.py z jasno określoną kolejnością middleware:

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.gzip import GZipMiddleware

app = FastAPI(
    title="Blake Crosley",
    docs_url=None,     # Disable docs in production
    redoc_url=None,
    openapi_url=None,  # Prevent /openapi.json exposure
)

# Middleware order matters: last added = first executed
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=500)
app.add_middleware(LocaleMiddleware)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(SecurityLogMiddleware, site_name="blakecrosley.com")

# Static files
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")

# Templates
templates = Jinja2Templates(directory=TEMPLATES_DIR)

Istotne są tu trzy decyzje projektowe. Po pierwsze, docs_url=None i openapi_url=None wyłączają automatyczne endpointy dokumentacji API. Publiczna strona z treścią nie potrzebuje udostępniać w internecie /docs ani /openapi.json.8 Po drugie, kolejność middleware ma znaczenie — rejestrowanie zdarzeń bezpieczeństwa wykonuje się jako pierwsze (jest dodawane jako ostatnie), dzięki czemu przechwytuje każde żądanie, także te odrzucone przez ograniczanie liczby żądań. Po trzecie, GZipMiddleware kompresuje odpowiedzi większe niż 500 bajtów, co zazwyczaj zmniejsza rozmiar transferu HTML o 70–80%. Od wersji Starlette 1.5.0 nie kompresuje już wszystkiego: domyślna lista wykluczeń pomija już skompresowane i binarne payloady (archiwa gzip i zip, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/*, fonty WOFF i WOFF2 oraz text/event-stream), czyli dokładnie to, czego potrzeba — ponowna kompresja PNG zużywa CPU, aby nieznacznie zwiększyć jego rozmiar. Warto zauważyć, że lista celowo wyklucza image/*, więc image/svg+xml nadal jest kompresowany. Można ją zastąpić parametrem nazwanym exclude_content_types.28

Routing

Trasy dzielą się na dwie kategorie: trasy stron zwracają pełne dokumenty HTML, a trasy API zwracają fragmenty JSON lub HTML.

# routes/pages.py — full HTML responses
from fastapi import APIRouter, Request

router = APIRouter()

@router.get("/about")
async def about(request: Request):
    templates = request.app.state.templates
    return templates.TemplateResponse("pages/about.html", {
        "request": request,
        "page_title": "About — Blake Crosley",
        "page_description": "Designer, developer, dad.",
    })
# routes/api.py — JSON or HTML fragment responses
@router.get("/api/quiz/{quiz_id}/step")
async def quiz_step(request: Request, quiz_id: str, answers: str = ""):
    # Parse answers, compute next question or result
    question = get_next_question(quiz_id, answers)
    templates = request.app.state.templates
    return templates.TemplateResponse("components/_quiz_step.html", {
        "request": request,
        "question": question,
        "answers": answers,
        "step": len(answers.split(",")) if answers else 0,
    })

To rozróżnienie ma znaczenie dla HTMX. Pełne trasy stron zwracają dokumenty rozszerzające base.html. Trasy API zwracają fragmenty HTML, które HTMX podmienia w istniejących elementach DOM. Oba typy renderuje ten sam silnik szablonów Jinja2 — bez osobnej warstwy API.

Wstrzykiwanie zależności

System Depends() w FastAPI zapewnia wyraźne rozdzielenie między handlerami tras a współdzieloną logiką:

from fastapi import Depends, Request

def get_templates(request: Request):
    """Get templates from app state."""
    return request.app.state.templates

def get_current_locale(request: Request) -> str:
    """Get locale from middleware-set request state."""
    return getattr(request.state, "locale", "en")

@router.get("/blog/{slug}")
async def blog_post(
    request: Request,
    slug: str,
    templates=Depends(get_templates),
    locale: str = Depends(get_current_locale),
):
    post = load_post_by_slug(slug)
    if not post:
        raise HTTPException(404, "Post not found")
    return templates.TemplateResponse("pages/blog/post.html", {
        "request": request,
        "post": post,
        "locale": locale,
    })

Zależności można składać. Zależność get_db może zależeć od get_current_locale, która z kolei zależy od żądania. FastAPI automatycznie rozwiązuje ten łańcuch.

Ustawienia Pydantic

Konfiguracja korzysta z BaseSettings Pydantic z priorytetem zmiennych środowiskowych:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    D1_WORKER_URL: str = ""
    D1_AUTH_SECRET: str = ""
    CLOUDFLARE_ACCOUNT_ID: str = ""
    CLOUDFLARE_API_TOKEN: str = ""
    ANALYTICS_PASSKEY: str = ""

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()

Zmienne środowiskowe nadpisują wartości z pliku .env. W środowisku produkcyjnym (Railway) sekrety są ustawiane jako zmienne środowiskowe. Lokalnie plik .env zapewnia wartości domyślne. Klasa Settings weryfikuje typy podczas uruchamiania — brak wymaganego pola powoduje szybki błąd zamiast błędu w czasie działania.

Wzorce async

Trasy FastAPI są domyślnie asynchroniczne. W przypadku operacji związanych z I/O (zapytań do bazy danych, żądań HTTP, odczytu plików) async zapobiega blokowaniu pętli zdarzeń:

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load translations into memory cache at startup
    async with httpx.AsyncClient() as client:
        for locale in SUPPORTED_LOCALES:
            resp = await client.post(f"{D1_URL}/query", ...)
            TRANSLATIONS[locale] = resp.json()["results"]
    yield
    # Cleanup on shutdown (if needed)

app = FastAPI(lifespan=lifespan)

Lifespan jest teraz jedyną ścieżką uruchamiania i zamykania. Starlette osiągnął pierwsze stabilne wydanie, 1.0, w marcu 2026 roku (wersja 1.6.0 na 8 sierpnia) i usunął od dawna wycofane hooki on_event, on_startup oraz on_shutdownlifespan (powyżej) jest jedynym mechanizmem, a @app.route() / @app.websocket_route() ustąpiły miejsca Route / WebSocketRoute na liście routes. FastAPI 0.137.0 (14 czerwca 2026) refaktoryzuje własne wewnętrzne mechanizmy routera: router.routes nie jest już płaską listą obiektów APIRoute, lecz drzewem węzłów pośrednich, dlatego należy traktować je jako szczegół implementacyjny, a nie coś do iterowania. Zaletą jest to, że trasy dodane do routera po include_router() są teraz odzwierciedlane na bieżąco, a podrouter można dołączyć przed zdefiniowaniem jego tras. Sam FastAPI nie przypina Starlette do linii 1.x: od 0.136.3 jego wymóg środowiska uruchomieniowego jest jedynie dolnym progiem, starlette>=0.46.0, i pozostaje niezmieniony do 0.140.7 — bez górnej granicy, a Starlette 0.4x nadal go spełnia. Numery wersji 1.x w informacjach o wydaniu 0.137.0 to aktualizacje Dependabot własnego pliku blokady testów repozytorium, a nie ograniczenie środowiska uruchomieniowego aplikacji.24 Żadne z tych zmian nie wpływa na wzorce w tym przewodniku — wszędzie wykorzystuje on lifespan i standardowe deklaracje tras — ale jeśli utrzymywane są narzędzia przechodzące po router.routes albo nadal używane są starsze handlery @app.on_event, wersje 0.137.0 / Starlette 1.0 są niekompatybilne wstecznie. FastAPI 0.137.2 (18 czerwca 2026) uzupełnia to o iter_route_contexts(), wspierany obecnie sposób wyliczania tras, gdy router.routes jest wewnętrzne. FastAPI 0.138.0 (20 czerwca 2026) dodaje następnie app.frontend("/", directory="dist") / router.frontend(...) do serwowania zbudowanego statycznego frontendu — przydatne w przypadku publikowania osobnego builda SPA, ale niezwiązane z podejściem tego przewodnika bez etapu buildowania i z renderowaniem po stronie serwera (montuje katalog dist/, zamiast renderować HTML na serwerze).25 FastAPI 0.139.0 (1 lipca 2026) rozszerza to o obsługę zależności w app.frontend() — na przykład automatyczne uwierzytelnianie przez cookie dla serwowanego frontendu — przenosząc ten sam mechanizm Depends(), używany na trasach API, do montowania statycznego frontendu.26 FastAPI 0.141.0 (29 lipca 2026) dodaje app.frontend(check_dir="auto"), które zapobiega błędowi fastapi dev, gdy katalog builda jeszcze nie istnieje — co zwykle występuje, gdy serwer zostaje uruchomiony przed wykonaniem builda frontendu. Wydane tego samego dnia FastAPI 0.141.1 naprawia zależności w app.frontend(), które po cichu pomijały zadania w tle i nagłówki odpowiedzi: zależność ustawiająca cookie lub planująca BackgroundTask traciła tę pracę na montowaniu frontendu, choć działała normalnie na trasach API. W przypadku wdrożenia obsługi zależności z 0.139.0, to wydanie 0.141.1 sprawia, że zachowuje się ona jak reszta aplikacji.29

FastAPI 0.140.0 kończy regresję pamięci obecną w każdym wydaniu od listopada 2025 roku — warto zaktualizować. Wydanie z 24 lipca 2026 zawiera jedną refaktoryzację o nieproporcjonalnie dużym wpływie. Dependant, wewnętrzny obiekt tworzony przez FastAPI dla każdego węzła grafu zależności każdej trasy, od wersji 0.121.0 (3 listopada 2025) gromadził atrybuty functools.cached_property — do wersji 0.139.2 było ich dziesięć. Właściwość cache’owana potrzebuje __dict__ dla każdej instancji, aby zapisać w nim wynik, więc koszt mnożył się przez każdy węzeł każdego grafu w aplikacji. PR #16049 przenosi tę logikę poza klasę do helperów na poziomie modułu (_get_cache_key(), _get_oauth_scopes(), _uses_scopes()) i deklaruje Dependant jako @dataclass(slots=True), pozostawiając go czystym kontenerem danych. Własny test CodSpeed FastAPI dla zmergowanego PR wskazuje, że benchmark pamięci test_dependency_graph wynosi 17,5 MB → 1,1 MB, czyli redukcję ×16; zgłoszenie, które zapoczątkowało te prace, opisywało usługę produkcyjną zużywającą mniej niż około 400 MB w 0.120.4 i kończącą się błędem OOM w 0.121.3. Zawierała ją każda wersja zalecana przez ten przewodnik od tamtej pory — 0.137.x, 0.138.0, 0.139.2. Jeśli aplikacja ma głębokie lub szerokie drzewo zależności (zagnieżdżone Depends(), schematy bezpieczeństwa, wiele dołączonych routerów), 0.140.0 zapewnia bezpłatną korzyść pamięciową bez zmian w kodzie aplikacji.27

0.140.0 było pierwszym krokiem, a nie pełną poprawką — należy przypiąć 0.140.7 lub nowszą wersję. Trzy dni po tym wydaniu, 27 lipca 2026, FastAPI wydał kolejnych siedem wersji w ciągu pięciu i pół godziny: od 0.140.1 do 0.140.7, z których każda refaktoryzowała ten sam mechanizm zależności. Prace dzielą się na dwie części. Pierwsza dotyczy płaskiego drzewa zależności: wcześniej FastAPI budował i przechowywał spłaszczoną kopię grafu zależności każdej trasy, a wersja 0.140.2 przestaje ją zachowywać, przy czym 0.140.3, 0.140.5, 0.140.6 i 0.140.7 usuwają pozostałe miejsca, które ją odtwarzały — generowanie OpenAPI, pola body, parametry żądań oraz ponownie OpenAPI. Wersja 0.140.4 usuwa księgowanie śledzące powtarzające się zależności, którego nic nie odczytywało. Druga część, i jedyna zmiana mająca widoczny próg: 0.140.1 zwiększa lru_cache pomocników klasyfikujących callable w fastapi/dependencies/models.py z 1 024 do 4 096 wpisów, za stałą o nazwie _CALLABLE_CLASSIFICATION_CACHE_SIZE, ponieważ użytkownicy zgłaszali aplikacje z ponad 1 024 odrębnymi zależnościami powodującymi ciągłe opróżnianie cache. Nic z tego nie zmienia wywoływanego API, więc aktualizacja sprowadza się do zmiany wersji. Warto jasno wskazać dwa zastrzeżenia: tempo wydań oznacza, że ta linia nadal się zmienia, dlatego należy czytać informacje o wydaniu, zamiast zakładać, że 0.140.7 to jej koniec; oraz że FastAPI dodał benchmarki zależności OpenAPI, mierzące te prace, w tym samym okresie (PR #16075), więc opublikowane wyniki obejmują kilka ostatnich wydań, a nie cały siedmiowersyjny okres.30

Operacje obciążające CPU (renderowanie Markdown, ekstrakcja CSS) mogą korzystać z funkcji synchronicznych. FastAPI uruchamia je automatycznie w puli wątków, gdy handler trasy nie jest zadeklarowany jako async:

# Sync function — FastAPI runs it in a thread pool
@router.get("/blog/{slug}")
def blog_post(slug: str):
    post = load_post_by_slug(slug)  # CPU-bound Markdown parsing
    return templates.TemplateResponse(...)

Zasada: jeśli funkcja oczekuje na I/O, należy użyć async. Jeśli wykonuje pracę CPU, należy pozostawić ją synchroniczną. Nie należy mieszać await z blokującymi wywołaniami w tej samej funkcji.9

Szablony Jinja2

Dziedziczenie szablonów

System dziedziczenia Jinja2 zastępuje kompozycję komponentów z React prostszym modelem. Jeden szablon bazowy definiuje szkielet strony. Szablony potomne wypełniają nazwane bloki:

<!-- base.html — the skeleton -->
<!DOCTYPE html>
<html lang="{{ lang_attr() }}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ page_title | default("Blake Crosley") }}</title>
  <meta name="description" content="{{ page_description | default('...') }}">

  <!-- CSS — single file, no preprocessor -->
  <link rel="stylesheet" href="{{ asset('css/styles.css') }}">

  <!-- JSON-LD structured data -->
  <script type="application/ld+json">
  { "@context": "https://schema.org", "@graph": [...] }
  </script>

  {% block head %}{% endblock %}
</head>
<body>
  <header class="header">...</header>

  <main id="main" role="main">
    {% block content %}{% endblock %}
  </main>

  <footer class="footer">...</footer>

  <!-- Scripts deferred for performance -->
  <script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
  <script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
  <script defer src="{{ asset('js/main.js') }}"></script>

  {% block scripts %}{% endblock %}
</body>
</html>
<!-- pages/about.html — fills the blocks -->
{% extends "base.html" %}

{% block head %}
<script type="application/ld+json">
{ "@type": "AboutPage", "name": "About Blake Crosley", ... }
</script>
{% endblock %}

{% block content %}
<section class="hero">
  <h1>About</h1>
  <p>Designer, developer, dad.</p>
</section>
{% endblock %}

Dyrektywa {% extends %} ustanawia relację rodzic-potomek. Szablon potomny definiuje jedynie te bloki, które chce nadpisać. Cała reszta — <head>, nagłówek, stopka, tagi skryptów — pochodzi z szablonu bazowego. To kompozycja przez odejmowanie, a nie budowanie.

Funkcja globalna asset()

Zasoby statyczne wykorzystują wersjonowanie oparte na haszu zawartości do wymuszania odświeżenia pamięci podręcznej:

# cache_assets.py
def build_asset_map(static_dir: Path) -> dict[str, str]:
    """Compute MD5 hashes of all static files at startup."""
    asset_map = {}
    for filepath in static_dir.rglob("*"):
        if filepath.is_file():
            rel_path = str(filepath.relative_to(static_dir))
            content_hash = hashlib.md5(filepath.read_bytes()).hexdigest()[:10]
            asset_map[rel_path] = content_hash
    return asset_map

def make_asset_url(asset_map: dict, path: str) -> str:
    """Generate versioned URL: /static/css/styles.css?v=a3f8b2c1d0"""
    clean_path = path.lstrip("/")
    version = asset_map.get(clean_path, "0")
    return f"/static/{clean_path}?v={version}"

W szablonie: {{ asset('css/styles.css') }} renderuje się jako /static/css/styles.css?v=a3f8b2c1d0. Hasz zmienia się wraz ze zmianą pliku, wymuszając odświeżenie pamięci podręcznej CDN. To zastępuje strategię nazw plików [contenthash] z webpack za pomocą 30 linii Python obliczanych przy starcie.

Include dla komponentów wielokrotnego użytku

Komponenty powtarzające się na wielu stronach wykorzystują {% include %}:

<!-- base.html -->
{% include "components/_language_switcher.html" %}
<!-- components/_language_switcher.html -->
{%- set current = current_locale() -%}
{%- set locales = all_locales() -%}

<div class="language-switcher"
     x-data="{ open: false }"
     @click.away="open = false">
  <button @click="open = !open" :aria-expanded="open">
    {{ current_locale_native() }}
  </button>
  <ul class="language-switcher-menu"
      :class="{ 'is-open': open }"
      x-cloak>
    {% for locale in locales %}
    <li>
      <a href="{{ locale_url(request.url.path, locale.code) }}"
         hreflang="{{ locale.code }}">
        {{ locale.native }}
      </a>
    </li>
    {% endfor %}
  </ul>
</div>

Prefiks z podkreślnikiem (_language_switcher.html) to konwencja oznaczająca partial — fragment szablonu nieprzeznaczony do samodzielnego renderowania. Ten komponent wykorzystuje zarówno Alpine.js (do przełączania rozwijanego menu), jak i Jinja2 (do listy lokalizacji). Granica jest wyraźna: Alpine.js zarządza stanem otwierania/zamykania, Jinja2 zarządza danymi.

Makra jako komponenty wielokrotnego użytku

Makra to funkcje Jinja2 — bloki szablonów wielokrotnego użytku z parametrami:

<!-- components/_macros.html -->
{% macro card(title, description, href, badge=None) %}
<article class="card">
  <a href="{{ href }}" class="card__link">
    {% if badge %}
    <span class="card__badge">{{ badge }}</span>
    {% endif %}
    <h3 class="card__title">{{ title }}</h3>
    {% if description %}
    <p class="card__description">{{ description }}</p>
    {% endif %}
  </a>
</article>
{% endmacro %}

{% macro optimized_image(image_config, loading="lazy") %}
{% if image_config.get("svg") %}
  <img src="{{ image_config.svg }}"
       width="{{ image_config.width }}"
       height="{{ image_config.height }}"
       alt="{{ image_config.alt }}">
{% else %}
  <picture>
    <source type="image/webp"
            srcset="{{ image_config.webp_srcset }}"
            sizes="(max-width: 768px) 100vw, 50vw">
    <img src="{{ image_config.fallback }}"
         width="{{ image_config.width }}"
         height="{{ image_config.height }}"
         alt="{{ image_config.alt }}"
         loading="{{ loading }}">
  </picture>
{% endif %}
{% endmacro %}

Importowanie i używanie makr w szablonach stron:

{% from "components/_macros.html" import card, optimized_image %}

<section class="projects">
  {% for project in projects %}
    {{ card(
      title=project.title,
      description=project.description,
      href=project.link,
      badge="New" if project.is_new else None
    ) }}
  {% endfor %}
</section>

Makra zastępują komponenty React w przypadku wzorców prezentacyjnych. Przyjmują parametry, obsługują wartości domyślne i komponują się z innymi makrami. Kluczowa różnica: makra renderują się jednokrotnie na serwerze i generują statyczny HTML. Komponenty React renderują się po stronie klienta i utrzymują stan. Do wyświetlania treści makra są odpowiednim narzędziem.

Kontekst szablonów i zmienne globalne

Zmienne globalne Jinja2 to funkcje dostępne w każdym szablonie bez konieczności ich jawnego przekazywania:

# In main.py — register globals
templates.env.globals["asset"] = lambda path: make_asset_url(_asset_map, path)
templates.env.globals["csrf_token"] = generate_csrf_token
templates.env.globals["analytics_script"] = analytics.tracking_script

Funkcja globalna asset() generuje wersjonowane adresy URL. Funkcja globalna csrf_token() generuje świeże tokeny CSRF. Funkcja globalna analytics_script() wstrzykuje fragment kodu śledzenia. Wszystkie te funkcje można wywoływać w dowolnym szablonie, bez konieczności jawnego przekazywania ich przez handler trasy.

W przypadku i18n konfiguracja jest bardziej złożona — funkcje tłumaczące potrzebują dostępu do lokalizacji bieżącego żądania:

# i18n/jinja.py
def setup_i18n_jinja(env):
    """Register translation functions as Jinja2 globals."""
    env.globals["_"] = get_translation        # _('ui.nav.about')
    env.globals["locale_prefix"] = get_locale_prefix  # '/ja' or ''
    env.globals["current_locale"] = get_current_locale
    env.globals["all_locales"] = get_all_locales
    env.globals["alternate_urls"] = get_alternate_urls
    env.globals["lang_attr"] = get_lang_attr  # 'ja' for HTML lang
    env.globals["og_locale"] = get_og_locale  # 'ja_JP' for og:locale
    env.globals["jsonld_lang"] = get_jsonld_lang  # 'ja-JP' for JSON-LD

Każda funkcja odczytuje lokalizację ze zmiennej kontekstowej żądania ustawianej przez middleware lokalizacji. Szablon wywołuje {{ _('ui.nav.about') }} i otrzymuje przetłumaczony ciąg znaków dla lokalizacji bieżącego żądania, bez konieczności jawnego przekazywania parametru lokalizacji.

Bloki warunkowe

System bloków Jinja2 obsługuje warunkowe nadpisywanie:

<!-- base.html -->
{% block head %}{% endblock %}

<!-- pages/blog/post.html -->
{% block head %}
<script type="application/ld+json">
{
  "@type": "Article",
  "headline": "{{ post.meta.title }}",
  "author": { "@id": "https://blakecrosley.com/#person" },
  "datePublished": "{{ post.meta.date.isoformat() }}",
  "dateModified": "{{ post.meta.updated.isoformat() if post.meta.updated else post.meta.date.isoformat() }}"
}
</script>

{% if post.meta.scripts %}
{% for script in post.meta.scripts %}
<script defer src="{{ asset(script.lstrip('/static/')) }}"></script>
{% endfor %}
{% endif %}

{% if post.meta.styles %}
{% for style in post.meta.styles %}
<link rel="stylesheet" href="{{ asset(style.lstrip('/static/')) }}">
{% endfor %}
{% endif %}
{% endblock %}

Wpisy blogowe deklarują swoje zależności w frontmatter YAML (scripts: ["/static/js/boids.js"]). Szablon warunkowo je dołącza. Strony, które nie potrzebują dodatkowych skryptów ani stylów, nie ładują żadnych — bez martwego kodu, bez nieużywanych importów.

Filtry niestandardowe

Filtry Jinja2 przekształcają dane podczas renderowania. Filtr sanitize zapobiega atakom XSS w treściach generowanych przez użytkowników:

import nh3

ALLOWED_TAGS = {"a", "b", "blockquote", "br", "code", "em", "h1", "h2",
                "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol",
                "p", "pre", "span", "strong", "table", "td", "th", "tr", "ul"}

def sanitize_html(value: str) -> str:
    """Sanitize HTML to prevent XSS attacks."""
    if not value:
        return ""
    return nh3.clean(
        value,
        tags=ALLOWED_TAGS,
        attributes={"a": {"href", "title"}, "img": {"src", "alt"}},
        link_rel="noopener noreferrer",
    )

templates.env.filters["sanitize"] = sanitize_html

W szablonach: {{ user_content | sanitize }}. Biblioteka nh3 to napisany w Rust sanitizer HTML — szybki i bezpieczny. Usuwa wszelkie tagi i atrybuty spoza listy dozwolonych, zapobiegając stored XSS nawet wtedy, gdy treść pochodzi z niezaufanego źródła.10


HTMX — szczegółowe omówienie

HTMX umożliwia dowolnemu elementowi HTML wysyłanie żądań HTTP i podmianę odpowiedzi w drzewie DOM. Kluczowa idea ma charakter architektoniczny: renderowany po stronie serwera HTML stanowi API. Serwer zwraca finalną reprezentację. Bez renderowania po stronie klienta, bez serializacji JSON, bez hydratacji.

Podstawowe atrybuty

Atrybut Przeznaczenie Przykład
hx-get Wysłanie żądania GET hx-get="/search?q=term"
hx-post Wysłanie żądania POST hx-post="/contact"
hx-target Miejsce umieszczenia odpowiedzi hx-target="#results"
hx-swap Sposób wstawienia odpowiedzi hx-swap="innerHTML" (domyślnie), outerHTML, beforeend
hx-trigger Zdarzenie wyzwalające żądanie hx-trigger="click", keyup changed delay:300ms, load
hx-indicator Element wyświetlany podczas żądania hx-indicator="#spinner"
hx-push-url Aktualizacja adresu URL przeglądarki hx-push-url="true"
hx-replace-url Zamiana URL bez wpisu w historii hx-replace-url="true"

Wzorzec 1: Interaktywny quiz (wieloetapowy stan na serwerze)

Strona blakecrosley.com zawiera interaktywny quiz, który prowadzi użytkowników przez wybór narzędzi. Cały stan quizu znajduje się na serwerze — bez zarządzania stanem po stronie klienta:

<!-- _quiz_container.html — initial load -->
<div hx-get="/api/quiz/claude-vs-codex/step?answers="
     hx-trigger="load"
     hx-swap="innerHTML"
     id="quiz-wrapper">
  <p>Loading quiz...</p>
</div>
<!-- _quiz_step.html — each question -->
<div class="quiz-step" id="quiz-container">
  <p>Question {{ step }} of {{ total }}</p>
  <h3>{{ question.question }}</h3>
  <div class="quiz-step__options">
    {% for opt in question.options %}
    <button class="quiz-step__btn"
            hx-get="/api/quiz/claude-vs-codex/step?answers={{ answers }},{{ opt.value }}"
            hx-target="#quiz-container"
            hx-swap="outerHTML">
      {{ opt.label }}
    </button>
    {% endfor %}
  </div>
</div>

Każde kliknięcie przycisku wysyła dotychczas zebrane odpowiedzi jako parametr zapytania. Serwer na podstawie historii odpowiedzi oblicza kolejne pytanie lub wynik końcowy. Stan kumuluje się w adresie URL — bez ciasteczek, bez sesji, bez JavaScript po stronie klienta. Quiz postępuje poprzez podmiany outerHTML: każda odpowiedź zastępuje cały element kroku quizu.

Wzorzec 2: Stronicowana lista wpisów na blogu

Strona z wpisami wykorzystuje HTMX do płynnej paginacji z aktualizacją adresu URL:

<!-- Pagination link -->
<a href="/writing?page=2&category=Engineering"
   hx-get="/writing?page=2&category=Engineering"
   hx-target="#writing-content"
   hx-swap="innerHTML"
   hx-replace-url="true"
   hx-indicator="#writing-loading"
   aria-label="Go to page 2">
  2
</a>

Cztery atrybuty współpracujące ze sobą:

  1. hx-get wysyła żądanie pod ten sam adres URL co href (stopniowe wzbogacanie — działa bez JavaScript)
  2. hx-target umieszcza odpowiedź w kontenerze #writing-content
  3. hx-replace-url="true" aktualizuje adres URL przeglądarki bez dodawania wpisu do historii
  4. hx-indicator wyświetla wskaźnik ładowania podczas trwania żądania

Serwer wykrywa żądania HTMX za pomocą nagłówka HX-Request i zwraca jedynie fragment z listą wpisów zamiast pełnej strony. Dlatego middleware nagłówków bezpieczeństwa dodaje Vary: HX-Request — aby pamięci podręczne CDN przechowywały pełną stronę i fragment osobno.11

Wzorzec 3: Wyszukiwanie z opóźnieniem (debounce)

<input type="search" name="q"
       hx-get="/api/search"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#results"
       hx-indicator="#search-spinner" />
<div id="results"></div>

Atrybut hx-trigger łączy trzy modyfikatory:

  • keyup — wyzwalany przy zwolnieniu klawisza
  • changed — wyzwalany tylko wtedy, gdy wartość faktycznie się zmieniła (zapobiega duplikowaniu żądań przez klawisze modyfikujące)
  • delay:300ms — opóźnienie (debounce) — czeka 300 ms od ostatniego zdarzenia keyup przed wysłaniem żądania

Serwer zwraca wyrenderowany fragment HTML:

@router.get("/api/search")
async def search(request: Request, q: str = ""):
    results = search_content(q)
    return templates.TemplateResponse("components/_search_results.html", {
        "request": request,
        "results": results,
        "query": q,
    })

Bez stanu po stronie klienta. Bez biblioteki debounce. Bez useEffect. Szablon renderuje wyniki, HTMX podmienia je w DOM, a serwer pozostaje jedynym źródłem prawdy.

Wzorzec 4: Podmiany poza głównym celem (OOB Swaps)

Zdarza się, że pojedyncza akcja serwera musi zaktualizować wiele elementów DOM jednocześnie. Mechanizm podmian OOB (out-of-band) w HTMX obsługuje to bez orkiestracji po stronie klienta:

<!-- Server returns multiple elements in one response -->
<!-- Primary target: swapped normally via hx-target -->
<div id="cart-items">
  <ul>
    <li>Widget A — $29.99</li>
    <li>Widget B — $14.99</li>
  </ul>
</div>

<!-- OOB target: swapped independently via hx-swap-oob -->
<span id="cart-count" hx-swap-oob="true">2 items</span>
<span id="cart-total" hx-swap-oob="true">$44.98</span>

Atrybut hx-swap-oob="true" instruuje HTMX, aby znalazł element po id w dowolnym miejscu drzewa DOM i zastąpił go, niezależnie od wartości hx-target. Zastępuje to wzorzec „podnoszenia stanu” (lift state up) z React — serwer oblicza cały stan pochodny i wysyła finalny HTML dla każdego elementu w jednej odpowiedzi.

Formularz kontaktowy dobrze to ilustruje: wysłanie formularza może zastąpić jego treść komunikatem o sukcesie, a jednocześnie zaktualizować plakietkę powiadomień poprzez podmianę OOB:

HTMX potrafi „wzmocnić” standardowe linki nawigacyjne, aby korzystały z AJAX zamiast pełnego przeładowania strony:

<nav hx-boost="true">
  <a href="/about">About</a>
  <a href="/writing">Writing</a>
  <a href="/guides">Guides</a>
</nav>

Dzięki hx-boost="true" kliknięcie w link pobiera stronę przez AJAX, podmienia zawartość <body> i aktualizuje adres URL — bez pełnego przeładowania strony. Historia przeglądarki działa normalnie (przyciski wstecz/dalej). Jeśli JavaScript nie zadziała, linki funkcjonują jako standardowa nawigacja.

Korzyścią jest postrzegana szybkość: wzmocniona nawigacja sprawia wrażenie natychmiastowej, ponieważ przeglądarka nie musi ponownie parsować CSS, ponownie ewaluować skryptów ani ponownie renderować układu. Zmienia się jedynie zawartość <body>. Wzmocnione linki sprawdzają się doskonale w głównych elementach nawigacyjnych, dzięki czemu przejścia między stronami przypominają aplikację jednostronicową — bez architektury SPA.

Wzorzec 6: Nagłówki żądań HTMX

HTMX wysyła niestandardowe nagłówki z każdym żądaniem:

Nagłówek Wartość Zastosowanie
HX-Request true Wykrywanie żądań HTMX po stronie serwera
HX-Target ID elementu Informacja, który element otrzyma odpowiedź
HX-Trigger ID elementu Informacja, który element wyzwolił żądanie
HX-Current-URL Pełny URL Informacja o bieżącej stronie użytkownika

Serwer może wykorzystać HX-Request do zwracania różnych odpowiedzi:

@router.get("/writing")
async def writing(request: Request, page: int = 1, category: str = None):
    posts = load_all_posts(page=page, category=category)
    context = {"request": request, "posts": posts, "current_page": page}

    # HTMX request: return only the post list fragment
    if request.headers.get("HX-Request"):
        return templates.TemplateResponse(
            "pages/writing/_post_list.html", context
        )

    # Normal request: return the full page
    return templates.TemplateResponse("pages/writing/index.html", context)

Ten wzorzec podwójnej odpowiedzi stanowi rdzeń architektury. Pełne załadowanie strony zwraca kompletny dokument (szablon bazowy + zawartość strony). Nawigacja przez HTMX zwraca jedynie zmienioną treść. Decyduje serwer, nie klient.

Wzorzec 7: Stopniowe wzbogacanie (Progressive Enhancement)

Każdy link HTMX na blakecrosley.com zawiera standardowy atrybut href:

<a href="/writing?page=2"
   hx-get="/writing?page=2"
   hx-target="#writing-content"
   hx-swap="innerHTML">
  Next Page
</a>

Jeśli JavaScript nie załaduje się, href działa jako zwykły link. Jeśli HTMX się załaduje, przechwytuje kliknięcie i wykonuje podmianę AJAX. To właśnie stopniowe wzbogacanie: strona działa bez JavaScript, a HTMX wzbogaca doświadczenie, gdy jest dostępny.

Wzorzec 8: Stany ładowania

<button hx-post="/api/contact"
        hx-target="#form-result"
        hx-indicator="#submit-spinner">
  <span id="submit-spinner" class="htmx-indicator">Sending...</span>
  <span>Send Message</span>
</button>

HTMX dodaje klasę htmx-request do elementu wyzwalającego na czas trwania żądania. Atrybut hx-indicator wskazuje element, który staje się widoczny podczas żądania. Stylowanie odbywa się za pomocą CSS:

.htmx-indicator {
  display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  display: inline;
}

Bez zarządzania stanami ładowania. Bez useState(false). Bez setLoading(true). CSS obsługuje widoczność, HTMX przełącza klasę.


Wzorce Alpine.js

Alpine.js wypełnia lukę, którą pozostawia HTMX: stan istniejący wyłącznie po stronie klienta, który nigdy nie musi komunikować się z serwerem. Gdy użytkownik klika rozwijane menu i ono się otwiera, ten stan istnieje tylko w przeglądarce. Alpine.js zarządza nim za pomocą atrybutów HTML.

Zasada granicy

Granica między HTMX a Alpine.js jest precyzyjna:

Typ stanu Narzędzie Przykład
Wymaga danych z serwera HTMX Wyniki wyszukiwania, walidacja formularzy, paginacja
Istnieje tylko w przeglądarce Alpine.js Otwieranie/zamykanie menu, przełącznik menu mobilnego, widoczność modala
Łączy oba podejścia Oba Przełącznik języka (przełączanie Alpine.js, nawigacja w stylu HTMX)

Nawigacja mobilna

Szablon bazowy opakowuje cały nagłówek w komponent Alpine.js:

<div x-data="{ navOpen: false, langOpen: false }"
     @keydown.escape.window="navOpen = false; langOpen = false">

  <!-- Mobile hamburger button -->
  <button @click="navOpen = !navOpen; langOpen = false"
          :aria-expanded="navOpen"
          :class="navOpen ? 'nav__toggle is-open' : 'nav__toggle'"
          aria-label="Toggle navigation">
    <span class="nav__toggle-icon">
      <span class="nav__toggle-bar"></span>
      <span class="nav__toggle-bar"></span>
      <span class="nav__toggle-bar"></span>
    </span>
  </button>

  <!-- Mobile menu panel -->
  <div class="mobile-menu" x-show="navOpen" x-cloak>
    <nav class="mobile-menu__nav">
      <a href="/about" @click="navOpen = false">About</a>
      <a href="/#work" @click="navOpen = false">Work</a>
      <a href="/writing" @click="navOpen = false">Writing</a>
    </nav>
  </div>
</div>

Kluczowe wzorce Alpine.js:

  • x-data deklaruje zakres komponentu i stan początkowy
  • x-show przełącza widoczność na podstawie stanu (wykorzystuje CSS display: none)
  • x-cloak ukrywa element do momentu inicjalizacji Alpine.js (zapobiega migotaniu niestylizowanej treści)
  • @click wiąże procedury obsługi kliknięć z wyrażeniami
  • :aria-expanded (skrót od x-bind:aria-expanded) dynamicznie ustawia atrybuty
  • @keydown.escape.window nasłuchuje klawisza Escape globalnie, aby zamknąć panele

Komponent rozwijany

Przełącznik języka wykorzystuje Alpine.js do zarządzania stanem z @click.away do zamykania przy kliknięciu poza komponentem:

<div x-data="{ open: false }"
     @click.away="open = false"
     @keydown.escape.window="open = false">

  <button @click="open = !open"
          :aria-expanded="open"
          aria-haspopup="listbox">
    English
    <svg :class="{ 'rotated': open }">...</svg>
  </button>

  <ul :class="{ 'is-open': open }"
      :aria-hidden="!open"
      role="listbox"
      x-cloak>
    <li role="option">
      <a href="/ja/about">日本語</a>
    </li>
    <!-- more languages -->
  </ul>
</div>

Modyfikator @click.away zamyka menu rozwijane przy kliknięciu na zewnątrz. Alpine.js obsługuje to za pomocą jednego atrybutu — bez rejestrowania nasłuchiwania zdarzeń, bez czyszczenia, bez zarządzania referencjami.

Kiedy używać Alpine.js, a kiedy czystego JavaScript

Alpine.js sprawdza się, gdy:

  • Stan jest ograniczony do pojedynczego elementu DOM (menu rozwijane, modal, przełącznik)
  • Interakcje są binarne lub proste (otwórz/zamknij, pokaż/ukryj, przełącz)
  • Wiele elementów musi reagować na tę samą zmianę stanu
  • Atrybuty dostępności muszą być zsynchronizowane z widocznością

Czysty JavaScript sprawdza się, gdy:

  • Interakcja wymaga złożonych obliczeń (wizualizacje, symulacje)
  • Komponent ma własną pętlę renderowania (canvas, animacje)
  • Wydajność jest kluczowa (Alpine.js dodaje narzut na każdy komponent x-data)
  • Logika przekracza 20–30 linii wyrażeń Alpine.js

blakecrosley.com używa Alpine.js do nawigacji, przełączania języków i przełączników treści. 20 interaktywnych komponentów bloga (symulacja boidów, wizualizator kodu Hamminga itd.) korzysta z czystego JavaScript, ponieważ wymaga renderowania na canvas i złożonych maszyn stanów.


Kompleksowy przykład: filtrowanie kategorii na /writing

Ta sekcja śledzi rzeczywistą funkcję z produkcyjnego kodu przez wszystkie warstwy: trasę, szablon, interakcję HTMX, bezpieczeństwo, cachowanie i wyrenderowany wynik. Funkcja: zakładki kategorii na stronie z wpisami, które filtrują posty bloga bez pełnego przeładowania strony.

Trasa (app/routes/pages.py:508)

async def writing_listing(request: Request, page: int = 1, category: str | None = None):
    """Writing page — blog posts and external publications."""
    templates = get_templates(request)
    markdown_posts = load_all_posts(published_only=True)
    all_posts = CUSTOM_BLOG_POSTS + markdown_posts

    # Filter by category if specified
    if category and category in CATEGORY_MAP:
        display_name = CATEGORY_MAP[category]
        all_posts = [
            p for p in all_posts
            if _get_post_category(p).lower() == display_name.lower()
        ]

    # Pagination
    total_pages = max(1, (len(all_posts) + POSTS_PER_PAGE - 1) // POSTS_PER_PAGE)
    page = max(1, min(page, total_pages))
    paginated = all_posts[(page - 1) * POSTS_PER_PAGE : page * POSTS_PER_PAGE]

    template_context = {
        "request": request,
        "posts": paginated,
        "categories": categories,
        "current_category": category,
        "current_page": page,
        "total_pages": total_pages,
        # ... SEO: canonical, prev/next URLs
    }

    # HTMX partial: return just the post list fragment
    if request.headers.get("HX-Request"):
        return templates.TemplateResponse(
            "pages/writing/_post_list.html",
            template_context,
        )

    # Full page for direct navigation
    return templates.TemplateResponse(
        "pages/writing/index.html",
        template_context,
    )

Sprawdzenie nagłówka HX-Request to kluczowy wzorzec: ta sama trasa, te same dane, inny szablon. HTMX otrzymuje fragment. Przeglądarki otrzymują pełną stronę.

Zakładki kategorii (HTMX)

<!-- Category filter tabs -->
<nav class="writing-categories">
  <a href="/writing"
     hx-get="/writing"
     hx-target="#post-list"
     hx-push-url="true"
     class="category-tab {% if not current_category %}active{% endif %}">
    All ({{ total_posts }})
  </a>
  {% for cat in categories %}
  <a href="/writing?category={{ cat.slug }}"
     hx-get="/writing?category={{ cat.slug }}"
     hx-target="#post-list"
     hx-push-url="true"
     class="category-tab {% if current_category == cat.slug %}active{% endif %}">
    {{ cat.name }} ({{ cat.count }})
  </a>
  {% endfor %}
</nav>

<div id="post-list">
  {% include "pages/writing/_post_list.html" %}
</div>

Każda zakładka ma zarówno href (działa bez JavaScript), jak i hx-get (podmienia tylko listę postów). hx-push-url aktualizuje adres URL przeglądarki, dzięki czemu przefiltrowany widok można udostępniać i dodawać do zakładek.

Fragment (pages/writing/_post_list.html)

Fragment renderuje się identycznie niezależnie od tego, czy jest dołączony przy ładowaniu strony, czy podmieniony przez HTMX:

{% for post in posts %}
<article class="post-card">
  <a href="{{ locale_prefix() }}/blog/{{ post.meta.slug }}">
    <h3>{{ post.meta.title }}</h3>
    <p>{{ post.meta.description }}</p>
    <time>{{ post.meta.date }}</time> · {{ post.reading_time }}m
  </a>
</article>
{% endfor %}

Brak specjalnego znacznika HTMX we fragmencie. Brak logiki renderowania po stronie klienta. Ten sam HTML obsługuje zarówno początkowe ładowanie strony, jak i każde kolejne filtrowanie.

Bezpieczeństwo

Wartości kategorii są walidowane względem CATEGORY_MAP (słownika po stronie serwera) przed filtrowaniem. Nieprawidłowe kategorie są ignorowane, a nie zwracane w odpowiedzi. Żadne dane wejściowe użytkownika nie są interpolowane do SQL ani HTML. Nagłówek CSP blokuje skrypty inline.

Cachowanie

Odpowiedzi kategorii są dynamiczne (bez cache CDN). Natomiast zasoby statyczne (CSS, HTMX, Alpine.js) mają hash w nazwie i są cachowane bezterminowo po pierwszym pobraniu. Kolejne przełączenia kategorii przesyłają jedynie fragment HTML (~3–5 KB) — bez ponownego pobierania CSS, JS ani obrazów.

Co to demonstruje

Jedna funkcja, prawdziwy kod produkcyjny, zero narzędzi do budowania. Serwer filtruje i renderuje HTML. HTMX podmienia listę postów. Alpine.js nie jest zaangażowany (nie potrzeba stanu po stronie klienta). URL aktualizuje się, umożliwiając udostępnianie. Progresywne ulepszanie: zakładki działają jako zwykłe linki bez JavaScript. Łączna ilość niestandardowego JavaScript w tej funkcji: zero linii.


Opcjonalne rozszerzenia

Poniższe sekcje opisują wzorce uzupełniające podstawowy stos, które nie są używane na blakecrosley.com. Zostały uwzględnione, ponieważ reprezentują najczęstsze dodatki wprowadzane przez zespoły adoptujące tę architekturę.


Bootstrap 5 bez Sass

Uwaga: blakecrosley.com używa zwykłego CSS z właściwościami niestandardowymi — bez Bootstrap. Ta sekcja opisuje Bootstrap 5 jako opcję dla zespołów, które chcą korzystać z frameworka narzędziowego bez etapu budowania. Skompilowany CSS Bootstrap można załadować z CDN lub dołączyć do arkusza stylów. Poniższe wzorce są uniwersalne i działają równolegle z podejściem HTMX + Alpine.js opisanym w poprzednich sekcjach.

Bootstrap 5 zrezygnował z jQuery jako zależności i obsługuje samodzielne użycie CSS. Nie potrzeba Sass, PostCSS ani żadnego narzędzia budowania, aby korzystać z systemu siatki i klas narzędziowych Bootstrap.

Samodzielny hosting bez CDN

blakecrosley.com hostuje wszystkie biblioteki zewnętrzne lokalnie:

<!-- base.html — no CDN, no external requests -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>

Samodzielny hosting eliminuje zależności zewnętrzne, zapobiega awariom CDN wpływającym na działanie witryny i umożliwia niezmienne buforowanie z adresami URL zawierającymi skrót zawartości. Należy pobrać skompilowany CSS Bootstrap (nie źródła Sass) i umieścić go w static/css/vendor/.

System siatki

System siatki Bootstrap działa ze zwykłymi klasami HTML:

<div class="container">
  <div class="row">
    <div class="col-12 col-md-8">
      <article>Main content</article>
    </div>
    <div class="col-12 col-md-4">
      <aside>Sidebar</aside>
    </div>
  </div>
</div>

Bez mixinów Sass. Bez @include make-col(). Skompilowany CSS zawiera responsywne klasy siatki. W przypadku niestandardowych punktów przełamania wykraczających poza domyślne wartości Bootstrap wystarczy napisać zwykłe zapytania medialne CSS.

Nadpisywanie za pomocą zwykłego CSS

Domyślne style Bootstrap można nadpisać za pomocą właściwości niestandardowych CSS i standardowych selektorów:

/* Custom design tokens — no Sass, no Tailwind */
:root {
  --color-bg-dark:        #000000;
  --color-text-primary:   #ffffff;
  --color-text-secondary: rgba(255, 255, 255, 0.65);
  --color-text-tertiary:  rgba(255, 255, 255, 0.40);
  --spacing-sm:           1rem;
  --spacing-md:           1.5rem;
  --spacing-lg:           2rem;
  --gutter:               48px;
  --font-size-lg:         1.25rem;
}

/* Responsive override — the browser reads this at runtime */
@media (max-width: 768px) {
  :root {
    --gutter: var(--spacing-md);  /* 48px → 24px on mobile */
  }
}

/* Override Bootstrap's default body styles */
body {
  background: var(--color-bg-dark);
  color: var(--color-text-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

Właściwości niestandardowe CSS kaskadowo przechodzą przez DOM, są dziedziczone z elementów nadrzędnych i reagują na zapytania medialne w czasie wykonywania. Zmienne Sass kompilują się do wartości statycznych i znikają. To rozróżnienie ma znaczenie przy tworzeniu motywów: pojedyncza zmiana właściwości niestandardowej może zaktualizować każdą wartość pochodną bez ponownej kompilacji.12

Klasy narzędziowe a CSS komponentów

Klasy narzędziowe Bootstrap sprawdzają się przy jednorazowych odstępach i układach. CSS komponentów należy stosować do powtarzających się wzorców:

<!-- Bootstrap utility for one-off spacing -->
<div class="mt-4 mb-3 px-2">One-off layout</div>

<!-- Component class for repeated patterns -->
<article class="writing__item">
  <h3 class="writing__item-title">Post Title</h3>
  <p class="writing__item-description">Description</p>
</article>
/* Component CSS — BEM naming, reusable */
.writing__item {
  padding: var(--spacing-md);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  transition: background 0.15s ease;
}
.writing__item:hover {
  background: rgba(255, 255, 255, 0.03);
}
.writing__item-title {
  font-size: var(--font-size-lg);
  margin-bottom: 0.5rem;
}

Zasada jest następująca: klasy narzędziowe Bootstrap do mechaniki układu (marginesy, wypełnienia, flexbox). Niestandardowy CSS do tożsamości wizualnej (kolory, typografia, animacje). Nie należy mieszać klas narzędziowych ze stylowaniem komponentów w zakresie tego samego zagadnienia.


Internacjonalizacja i lokalizacja

blakecrosley.com udostępnia treści w 10 językach: angielskim, japońskim, koreańskim, chińskim uproszczonym, chińskim tradycyjnym, niemieckim, francuskim, hiszpańskim, polskim i portugalskim (brazylijskim).

Routing locale oparty na URL

Locale znajduje się w ścieżce URL: /about (angielski), /ja/about (japoński), /zh-Hans/about (chiński uproszczony). Angielski jest domyślny i nie ma prefiksu.

# i18n/config.py
SUPPORTED_LOCALES = [
    "en", "zh-Hans", "zh-Hant", "fr", "de", "ja", "ko", "pl", "pt-BR", "es"
]
DEFAULT_LOCALE = "en"

Middleware locale wyodrębnia locale ze ścieżki URL:

# i18n/middleware.py
class LocaleMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        path = request.url.path
        # Check if path starts with a supported locale
        for locale in SUPPORTED_LOCALES:
            if path.startswith(f"/{locale}/") or path == f"/{locale}":
                request.state.locale = locale
                # Strip locale prefix for route matching
                request.scope["path"] = path[len(f"/{locale}"):]
                break
        else:
            request.state.locale = DEFAULT_LOCALE

        response = await call_next(request)
        return response

Middleware usuwa prefiks locale przed dopasowaniem tras. Oznacza to, że handlery tras nie potrzebują ścieżek specyficznych dla locale — /about obsługuje zarówno angielski (/about), jak i japoński (/ja/about), ponieważ middleware normalizuje ścieżkę.

Funkcje tłumaczeń w szablonach

Zmienne globalne Jinja2 udostępniają funkcje tłumaczeń:

<!-- Template usage -->
<h3>{{ _('ui.footer.navigate') | default('Navigate') }}</h3>
<a href="{{ locale_prefix() }}/about">
  {{ _('ui.nav.about') | default('About') }}
</a>

Funkcja _() wyszukuje klucz tłumaczenia w pamięci podręcznej. Filtr | default() zapewnia angielski fallback w przypadku braku tłumaczenia. Funkcja locale_prefix() zwraca prefiks URL dla bieżącego locale ("" dla angielskiego, "/ja" dla japońskiego).

Tagi hreflang

Każda strona zawiera tagi hreflang dla wszystkich obsługiwanych locale:

<!-- Generated in base.html -->
{% for alt in alternate_urls(request.url.path) %}
<link rel="alternate" hreflang="{{ alt.hreflang }}" href="{{ alt.url }}">
{% endfor %}

Generuje to:

<link rel="alternate" hreflang="en" href="https://blakecrosley.com/about">
<link rel="alternate" hreflang="ja" href="https://blakecrosley.com/ja/about">
<link rel="alternate" hreflang="zh-Hans" href="https://blakecrosley.com/zh-Hans/about">
<!-- ... all 10 locales -->
<link rel="alternate" hreflang="x-default" href="https://blakecrosley.com/about">

Wyszukiwarki wykorzystują hreflang do wyświetlania prawidłowej wersji językowej w wynikach wyszukiwania. Wpis x-default wskazuje na wersję angielską jako fallback.13

Przechowywanie tłumaczeń i pamięć podręczna

Tłumaczenia są przechowywane w Cloudflare D1 (SQLite na brzegu sieci) i ładowane do pamięci podręcznej za pośrednictwem handlera lifespan:

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load translations into memory at startup
    for locale in SUPPORTED_LOCALES:
        data = await fetch_translations(locale)
        TRANSLATIONS[locale] = data
    yield

app = FastAPI(lifespan=lifespan)

Pamięć podręczna eliminuje zapytania do bazy danych przy każdym renderowaniu strony. Aktualizacja tłumaczeń wymaga odświeżenia cache (wyzwalanego przez endpoint administracyjny lub wdrożenie). Taka architektura poświęca świeżość danych na rzecz wydajności — tłumaczenia zmieniają się rzadko, natomiast renderowanie stron odbywa się przy każdym żądaniu.

Monitorowanie stanu

blakecrosley.com zawiera endpoint sprawdzania stanu i18n, który monitoruje pokrycie tłumaczeń dla poszczególnych locale:

@app.get("/health/i18n")
async def health_i18n():
    cache = get_translation_cache()
    result = {
        "status": "healthy",
        "cache_loaded": cache.is_loaded,
        "locales": {},
        "alerts": [],
    }

    # Check coverage for each locale
    for locale in SUPPORTED_LOCALES:
        coverage = await calculate_coverage(locale, en_count)
        result["locales"][locale] = {"coverage": round(coverage, 2)}

        if coverage < 99.5:
            result["alerts"].append(
                f"{locale}: {coverage:.1f}% coverage (threshold: 99.5%)"
            )
            result["status"] = "warning"

    return result

Próg pokrycia 99,5% wykrywa brakujące tłumaczenia, zanim użytkownicy napotkają nieprzetłumaczone ciągi tekstowe. Endpoint stanu integruje się z monitoringiem Railway, aby alertować o spadku pokrycia — na przykład po dodaniu nowych ciągów UI, które nie zostały jeszcze przetłumaczone.

Renderowanie treści z uwzględnieniem locale

Wpisy blogowe i przewodniki obsługują tłumaczenia metadanych i treści dla poszczególnych locale:

# In route handler
translated = get_blog_translation(post.meta.slug, locale)
return templates.TemplateResponse("pages/blog/post.html", {
    "request": request,
    "post": post,
    "translated_title": translated.title if translated else post.meta.title,
    "translated_description": translated.description if translated else post.meta.description,
})
<!-- In template -->
<h1>{{ translated_title }}</h1>
<p class="post__description">{{ translated_description }}</p>
<!-- Body content falls back to English if translation unavailable -->
{{ post.html | sanitize | safe }}

Wzorzec jest spójny: najpierw próba wyświetlenia przetłumaczonej treści, w razie jej braku — fallback do angielskiego. Umożliwia to częściowe tłumaczenie — japoński użytkownik widzi przetłumaczone tytuły i opisy, nawet jeśli pełna treść artykułu pozostaje w języku angielskim. Filtr | default() Jinja2 koduje ten wzorzec w jednym pipe:

{{ translated.title if translated else post.meta.title }}

Tłumaczenie danych locale

Treści statyczne, takie jak opisy projektów i etykiety nawigacji, są tłumaczone za pomocą funkcji pomocniczych, które zachowują tę samą strukturę danych, podmieniając ciągi tekstowe specyficzne dla danego locale:

# i18n/data.py
def translate_projects(projects: list, locale: str) -> list:
    """Return projects with translated titles and descriptions."""
    if locale == "en":
        return projects
    translated = []
    for project in projects:
        t = get_translation(f"project.{project['slug']}.title", locale)
        d = get_translation(f"project.{project['slug']}.description", locale)
        translated.append({
            **project,
            "title": t or project["title"],
            "description": d or project["description"],
        })
    return translated

Takie podejście utrzymuje warstwę tłumaczeń oddzieloną od warstwy danych. Trasy przekazują tę samą listę projects niezależnie od locale. Funkcje tłumaczeń opakowują dane w sposób transparentny.

Mapa witryny z alternatywami hreflang

Dynamiczna mapa witryny zawiera wszystkie strony we wszystkich locale z wzajemnymi odwołaniami:

@app.get("/sitemap.xml")
async def sitemap():
    for page in static_pages:
        for locale in SUPPORTED_LOCALES:
            # Each URL entry includes alternates for all locales
            locale_path = f"/{locale}{path}" if locale != "en" else path
            xml_parts.append(f"<loc>{base_url}{locale_path}</loc>")
            # Add xhtml:link alternates
            for alt_locale in SUPPORTED_LOCALES:
                alt_path = f"/{alt_locale}{path}" if alt_locale != "en" else path
                hreflang = LOCALE_TO_HREFLANG[alt_locale]
                xml_parts.append(
                    f'<xhtml:link rel="alternate" hreflang="{hreflang}" '
                    f'href="{base_url}{alt_path}"/>'
                )

Generuje to 10 wpisów URL na stronę (po jednym na locale), z których każdy zawiera 11 linków alternatywnych (10 locale + x-default). Dla witryny z 50 stronami mapa witryny zawiera 500 wpisów URL z 5500 linkami hreflang. Mapa witryny jest generowana dynamicznie i cache’owana na jedną godzinę.


Wzorce baz danych

Uwaga: blakecrosley.com używa Cloudflare D1 (serverless SQLite) przez HTTP dla wszystkich trwałych danych, a nie SQLAlchemy. Ta sekcja omawia standardowy wzorzec async SQLAlchemy dla projektów FastAPI, które potrzebują relacyjnej bazy danych — najczęstszej konfiguracji produkcyjnej dla tego stosu.

Async w SQLAlchemy 2.0

W aplikacjach, które potrzebują relacyjnej bazy danych, obsługa async w SQLAlchemy 2.0 dobrze integruje się z FastAPI:

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase

engine = create_async_engine("sqlite+aiosqlite:///./data.db")
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

class Base(DeclarativeBase):
    pass

Uwaga dotycząca instalacji (SQLAlchemy 2.0.50+): od wersji 2.0.50 zależność greenlet dla stosu async nie jest już instalowana domyślnie. Należy użyć dodatku asyncio, aby została pobrana, w przeciwnym razie pierwsze await względem silnika zakończy się błędem braku greenlet:23

pip install "sqlalchemy[asyncio]" aiosqlite

SQLAlchemy 2.0.50 wymaga także Python 3.10+ (porzucono obsługę 3.7–3.9) i dodaje koła dla free-threaded (3.13t).23

Dependency Injection dla sesji bazy danych

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise

@router.get("/users/{user_id}")
async def get_user(request: Request, user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(404, "User not found")
    return templates.TemplateResponse("pages/user.html", {
        "request": request, "user": user
    })

Zależność get_db zarządza cyklem życia sesji: otwiera sesję, przekazuje ją do route handlera, zatwierdza po powodzeniu i wycofuje w razie wyjątku. Każda operacja na bazie danych używa zapytań parametryzowanych — nigdy interpolacji ciągów znaków.

Integracja z Pydantic

Modele Pydantic walidują dane wejściowe na granicy API i serializują dane wyjściowe dla szablonów:

from pydantic import BaseModel, EmailStr

class ContactForm(BaseModel):
    name: str
    email: EmailStr
    message: str

@router.post("/contact")
async def submit_contact(request: Request, form: ContactForm):
    # form.name, form.email, form.message are validated
    await send_email(form)
    return templates.TemplateResponse("components/_contact_success.html", {
        "request": request
    })

Pydantic waliduje typy, formaty (email, URL) oraz ograniczenia (minimalna/maksymalna długość), zanim route handler zostanie wykonany. Nieprawidłowe dane wejściowe automatycznie zwracają odpowiedź 422. Zastępuje to biblioteki walidacji formularzy po stronie klienta — serwer waliduje dane, a HTMX podmienia albo komunikat sukcesu, albo informację zwrotną o błędzie.

Migracje z Alembic

Alembic zarządza zmianami schematu bazy danych:

# Generate a migration from model changes
alembic revision --autogenerate -m "add user preferences table"

# Apply migrations
alembic upgrade head

# Roll back one migration
alembic downgrade -1

Funkcja autogenerate porównuje modele SQLAlchemy z bieżącym schematem bazy danych i generuje skrypty migracji. Te skrypty to wersjonowane pliki Python, które znajdują się w repozytorium:

# alembic/versions/001_add_user_preferences.py
def upgrade():
    op.create_table(
        "user_preferences",
        sa.Column("id", sa.Integer, primary_key=True),
        sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id")),
        sa.Column("locale", sa.String(10), default="en"),
        sa.Column("theme", sa.String(20), default="dark"),
    )

def downgrade():
    op.drop_table("user_preferences")

Migracje są uruchamiane podczas wdrożenia (przed startem aplikacji). Dzięki temu schemat bazy danych odpowiada kodowi aplikacji. W przypadku blakecrosley.com większość danych znajduje się w Cloudflare D1 (dostęp przez HTTP), dlatego migracje Alembic dotyczą lokalnej bazy SQLite lub PostgreSQL używanej do danych sesji i analityki.

Wzorzec Cloudflare D1

blakecrosley.com używa Cloudflare D1 jako zdalnej bazy danych dostępnej przez proxy Cloudflare Worker:

class D1Client:
    """HTTP client for Cloudflare D1 via Worker proxy."""

    def __init__(self, worker_url: str, auth_secret: str):
        self.worker_url = worker_url
        self.auth_secret = auth_secret

    async def fetch_all(self, sql: str, params: list = None) -> list[dict]:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.worker_url}/query",
                json={"sql": sql, "params": params or []},
                headers={"Authorization": f"Bearer {self.auth_secret}"},
            )
            return response.json()["results"]

Ten wzorzec sprawdza się w aplikacjach, które potrzebują bazy danych, ale nie chcą zarządzać serwerem bazy danych. D1 to SQLite na brzegu sieci Cloudflare, dostępne przez HTTP. Proxy Worker obsługuje uwierzytelnianie i ograniczanie liczby żądań. Kompromisem jest opóźnienie: każde zapytanie jest żądaniem HTTP (~50-100 ms), podczas gdy lokalne połączenie z bazą danych zajmuje ~1-5 ms. Pamięć podręczna w pamięci operacyjnej inicjalizowana przy starcie łagodzi ten problem w obciążeniach z przewagą odczytu, takich jak tłumaczenia.


Bezpieczeństwo

Ograniczanie rozmiaru treści żądania

Starlette 1.6.0 dodał max_body_size, czyli brakującą w tym stosie kontrolę: bez niej klient może przesyłać do aplikacji strumień danych o nieograniczonym rozmiarze, czyniąc pamięć punktem awarii. Można ustawić ją dla Starlette, Router, Mount lub pojedynczego Route, a także opakować dowolną aplikację ASGI w RequestBodyLimitMiddleware. Zagnieżdżone trasy mogą zwiększać lub zmniejszać limit dla całej aplikacji, dzięki czemu endpoint przesyłania plików może być bardziej liberalny, podczas gdy pozostałe pozostają restrykcyjne.

app = FastAPI(lifespan=lifespan)
app.router.max_body_size = 2 * 1024 * 1024  # 2 MB default for the whole app

Limit zlicza bajty faktycznie odebrane z serwera ASGI, w tym dane plików multipart, a Content-Length traktuje wyłącznie jako mechanizm szybkiego odrzucenia — brakujący lub zaniżony nagłówek nie pozwoli go ominąć. Wartością domyślną jest None, czyli brak limitu, dlatego należy włączyć tę funkcję jawnie.28

Middleware nagłówków bezpieczeństwa

blakecrosley.com wdraża wzmocnione nagłówki bezpieczeństwa za pomocą własnego middleware:

class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    CSP_DIRECTIVES = {
        "default-src": "'self'",
        "script-src": "'self' 'unsafe-inline' 'unsafe-eval'",
        "style-src": "'self' 'unsafe-inline'",
        "img-src": "'self' data: https:",
        "connect-src": "'self'",
        "frame-ancestors": "'self'",
        "base-uri": "'self'",
        "form-action": "'self'",
        "upgrade-insecure-requests": "",
    }

    async def dispatch(self, request, call_next):
        response = await call_next(request)
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-Frame-Options"] = "SAMEORIGIN"
        response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
        response.headers["Strict-Transport-Security"] = (
            "max-age=31536000; includeSubDomains"
        )
        response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
        response.headers["Content-Security-Policy"] = self.csp
        response.headers["Permissions-Policy"] = self.PERMISSIONS_POLICY
        return response

CSP zawiera 'unsafe-inline' i 'unsafe-eval', ponieważ Alpine.js wymaga ich do obliczania wyrażeń. Alternatywą jest wersja Alpine.js zgodna z CSP, która ma ograniczenia.14 Wszystkie pozostałe funkcje są rygorystycznie ograniczone: frame-ancestors zapobiega clickjackingowi, form-action ogranicza wysyłanie formularzy do tego samego origin, a upgrade-insecure-requests wymusza HTTPS.

Bezpieczeństwo pamięci podręcznej CDN z HTMX

Middleware nagłówków bezpieczeństwa dodaje Vary: HX-Request do odpowiedzi HTMX:

if request.headers.get("HX-Request"):
    existing_vary = response.headers.get("Vary", "")
    if "HX-Request" not in existing_vary:
        parts = [v.strip() for v in existing_vary.split(",") if v.strip()]
        parts.append("HX-Request")
        response.headers["Vary"] = ", ".join(parts)

Bez tego nagłówka CDN mógłby zapisać w pamięci podręcznej odpowiedź fragmentu HTMX i zwrócić ją jako pełną stronę dla żądania bez HTMX (lub odwrotnie). Nagłówek Vary informuje CDN, aby przechowywał oddzielne wpisy pamięci podręcznej zależnie od wartości nagłówka HX-Request.11

Ochrona CSRF

Formularze HTMX używają bezstanowych tokenów CSRF podpisanych HMAC:

# csrf.py
def generate_csrf_token() -> str:
    """Token format: timestamp:random:HMAC-SHA256-signature"""
    timestamp = str(int(time.time()))
    random_value = secrets.token_hex(16)
    payload = f"{timestamp}:{random_value}"
    signature = hmac.new(
        CSRF_SECRET.encode(), payload.encode(), hashlib.sha256
    ).hexdigest()
    return f"{payload}:{signature}"

def validate_csrf_token(token: str) -> bool:
    """Verify signature and check expiration (1 hour)."""
    timestamp, random_value, signature = token.split(":")
    if int(time.time()) - int(timestamp) > 3600:
        return False
    expected = hmac.new(
        CSRF_SECRET.encode(),
        f"{timestamp}:{random_value}".encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Token jest generowany w szablonie za pośrednictwem globalnej funkcji Jinja2 i dołączany do żądań formularzy HTMX:

<form hx-post="/contact" hx-target="#form-result">
  <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
  <!-- form fields -->
</form>

Bezstanowe tokeny eliminują konieczność przechowywania sesji po stronie serwera. Podpis HMAC zapewnia, że token został wygenerowany przez serwer. Znacznik czasu zapobiega atakom powtórzeniowym. hmac.compare_digest zapobiega atakom timingowym.15

Sanityzacja HTML

Treści generowane przez użytkowników przechodzą przez nh3 przed renderowaniem:

templates.env.filters["sanitize"] = sanitize_html
# In templates: {{ content | sanitize }}

Biblioteka nh3 usuwa tagi i atrybuty spoza listy dozwolonych. Linki automatycznie otrzymują rel="noopener noreferrer". Ta ochrona jest niezależna od CSP — zapobiega przechowywanemu XSS na warstwie renderowania, podczas gdy CSP zapobiega wstrzykniętym skryptom na warstwie przeglądarki. Obrona wielowarstwowa.

Walidacja danych wejściowych

Modele Pydantic walidują wszystkie dane wejściowe na granicy API:

from pydantic import BaseModel, Field, EmailStr

class ContactRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    email: EmailStr
    message: str = Field(..., min_length=10, max_length=5000)

FastAPI automatycznie zwraca 422 Unprocessable Entity dla nieprawidłowych danych wejściowych. W połączeniu ze sparametryzowanymi zapytaniami do bazy danych (SQLAlchemy nigdy nie interpoluje ciągów znaków) zapobiega to SQL injection i zapewnia bezpieczeństwo typów na granicach systemu.


Wydajność

Lighthouse 100/100/100/100

blakecrosley.com uzyskuje wynik 100 we wszystkich czterech kategoriach Lighthouse: Performance, Accessibility, Best Practices i SEO. Można to sprawdzić w PageSpeed Insights.2

Kluczowe optymalizacje:

Strategia ładowania CSS

blakecrosley.com ładuje CSS za pomocą pojedynczego tagu <link> oraz adresów URL z hashem treści, umożliwiających niezmienne buforowanie:

<link rel="stylesheet" href="{{ asset('css/styles.css') }}">

Funkcja pomocnicza asset() dodaje hash treści (?v=a3b2c1d4), dzięki czemu przeglądarka przechowuje plik w pamięci podręcznej bezterminowo, dopóki jego treść się nie zmieni. Nie ma wyodrębniania krytycznego CSS, sztuczki z print-media ani ładowania opartego na JavaScript. Plik CSS ma około 8 KB po kompresji gzip — jest na tyle mały, że podejście z pojedynczym żądaniem uzyskuje 100 punktów w Lighthouse Performance bez akrobatyki optymalizacyjnej.

Kompresja GZip

app.add_middleware(GZipMiddleware, minimum_size=500)

Odpowiedzi większe niż 500 bajtów są kompresowane, z wyjątkiem domyślnych wykluczeń typów treści wprowadzonych przez Starlette 1.5.0 (archiwa, obrazy, audio, wideo, fonty, SSE). HTML kompresuje się o 70–80%, zmniejszając dokument o rozmiarze 15 KB do 3–4 KB.28

Niezmienne buforowanie zasobów statycznych

# In security headers middleware
if request.url.path.startswith("/static/"):
    if os.environ.get("RAILWAY_ENVIRONMENT"):
        response.headers["Cache-Control"] = "public, max-age=31536000, immutable"

Zasoby statyczne z adresami URL zawierającymi hash treści (?v=a3f8b2c1d0) są buforowane przez rok z dyrektywą immutable. Hash zmienia się wraz ze zmianą pliku, wymuszając pobranie nowej wersji przez przeglądarki i CDN.

Odroczone ładowanie skryptów

<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>

Atrybut defer pobiera skrypty równolegle z parsowaniem HTML, lecz wykonuje je dopiero po sparsowaniu dokumentu. Zapobiega to blokowaniu renderowania bez złożoności asynchronicznego ładowania i zarządzania kolejnością wykonywania.

Optymalizacja obrazów

Obrazy używają WebP z responsywnym srcset oraz jawnymi wymiarami:

OPTIMIZED_IMAGES = {
    "vision-sprint": {
        "webp_srcset": (
            "/static/images/optimized/vision-sprint-400w.webp 400w, "
            "/static/images/optimized/vision-sprint-800w.webp 800w, "
            "/static/images/optimized/vision-sprint-1200w.webp 1200w"
        ),
        "fallback": "/static/images/optimized/vision-sprint-fallback.jpg",
        "width": 1200,
        "height": 1045,
    },
}
<picture>
  <source type="image/webp"
          srcset="{{ image.webp_srcset }}"
          sizes="(max-width: 768px) 100vw, 50vw">
  <img src="{{ image.fallback }}"
       width="{{ image.width }}"
       height="{{ image.height }}"
       alt="{{ image.alt }}"
       loading="lazy">
</picture>

Jawne atrybuty width i height zapobiegają Cumulative Layout Shift (CLS). Atrybut loading="lazy" odracza ładowanie obrazów poza ekranem. WebP zapewnia pliki o 25–35% mniejsze niż JPEG przy porównywalnej jakości.16

Early Hints

# In main.py
app.state.preload_links = [
    f'<{make_asset_url(_asset_map, "css/styles.css")}>; rel=preload; as=style',
]

# In security headers middleware
if "text/html" in content_type:
    preload_links = getattr(request.app.state, "preload_links", [])
    if preload_links:
        response.headers["Link"] = ", ".join(preload_links)

Nagłówek Link z rel=preload informuje Cloudflare, aby wysłał odpowiedź 103 Early Hints, umożliwiając przeglądarce rozpoczęcie pobierania CSS przed zakończeniem generowania przez serwer odpowiedzi HTML.17

Minimalny JavaScript

Łączny rozmiar JavaScript:

Biblioteka Rozmiar (zminimalizowany + skompresowany gzip)
HTMX ~16 KB
Alpine.js ~15 KB
JS specyficzny dla strony 4–8 KB
Łącznie 35–39 KB

Typowa aplikacja React wysyła 100–300 KB frameworkowego JavaScript jeszcze przed kodem aplikacji.18 Podejście bez procesu budowania wysyła mniej JavaScript, ponieważ jest go mniej do wysłania.

Wdrożenie

Railway

Serwis blakecrosley.com jest wdrażany na platformę Railway za pomocą polecenia git push:

# railway.toml
[build]
builder = "nixpacks"

[deploy]
startCommand = "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"
healthcheckPath = "/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10

Kreator Nixpacks platformy Railway wykrywa projekt Python na podstawie pliku requirements.txt, instaluje zależności i uruchamia polecenie startowe. Plik Docker nie jest wymagany. Punkt końcowy kontroli kondycji zapewnia, że aplikacja odpowiada, zanim zacznie przyjmować ruch:

@app.get("/health")
async def health():
    return {"status": "healthy"}

Proces wdrażania

git push origin main
  → Railway detects push
  → Nixpacks installs Python + requirements.txt (cached)
  → uvicorn starts
  → Health check passes
  → Traffic routes to new deployment
  → ~40 seconds total

Bez npm install. Bez npm run build. Bez kompilacji przez webpack. Bez kompilacji TypeScript. Jedynym etapem instalacji jest pip install -r requirements.txt, którego wynik jest przechowywany w pamięci podręcznej między wdrożeniami.

Procfile

web: uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}

Plik Procfile stanowi alternatywę zgodną z Heroku. Railway obsługuje zarówno railway.toml, jak i Procfile. Składnia ${PORT:-8000} korzysta z portu udostępnionego przez platformę lub domyślnie z portu 8000 podczas programowania lokalnego.

Konfiguracja Uvicorn dla środowiska produkcyjnego

W przypadku wdrożeń obsługujących większy ruch należy użyć wielu procesów roboczych:

uvicorn app.main:app \
  --host 0.0.0.0 \
  --port ${PORT:-8000} \
  --workers 4 \
  --loop uvloop \
  --http httptools
  • --workers 4 uruchamia cztery procesy robocze (ogólna zasada: 2 * liczba rdzeni CPU + 1)
  • --loop uvloop korzysta z szybszej pętli zdarzeń uvloop (bezpośredniego zamiennika asyncio)
  • --http httptools korzysta z szybszego parsera HTTP httptools

Każdy proces roboczy jest osobnym procesem przechowującym własną kopię aplikacji, dlatego zużycie pamięci przez proces jest mnożone przez liczbę procesów roboczych — i właśnie tutaj poprawka grafu zależności w FastAPI 0.140.0 przynosi korzyści: w aplikacji z dużą liczbą zależności cztery procesy robocze w wersji 0.139.2 czterokrotnie ponoszą wcześniejszy narzut związany z Dependant.27

Podczas programowania opcja --reload monitoruje zmiany w plikach:

uvicorn app.main:app --reload --port 8000

Alternatywa z Docker

Na platformach wymagających Docker:

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Odchudzony obraz bazowy pozwala zachować niewielki rozmiar kontenera. Opcja --no-cache-dir zapobiega zapisywaniu przez pip pobranych pakietów w warstwie obrazu.

CDN Cloudflare

Serwis blakecrosley.com korzysta z Cloudflare do buforowania CDN, obsługi DNS i Workers:

# Cache headers for HTML pages (set in security middleware)
response.headers["Cache-Control"] = (
    "public, max-age=300, s-maxage=3600, "
    "stale-while-revalidate=86400"
)
  • max-age=300 — przeglądarka przechowuje dane w pamięci podręcznej przez 5 minut
  • s-maxage=3600 — CDN przechowuje dane w pamięci podręcznej przez 1 godzinę
  • stale-while-revalidate=86400 — nieaktualna zawartość jest udostępniana podczas jej ponownej walidacji przez 24 godziny

Zasoby statyczne otrzymują ustawienie max-age=31536000, immutable, ponieważ adresy URL z hashem zawartości gwarantują aktualność.


Ramy decyzyjne

Czy potrzebne są narzędzia do budowania?

Należy odpowiedzieć na cztery pytania:

1. Czy więcej niż pięciu programistów współdzieli interfejsy JavaScript? Jeśli tak, sprawdzanie typów podczas kompilacji zapewniane przez TypeScript zapobiega błędom integracji, które testy wykonywane w czasie działania wykrywają zbyt późno. Należy dodać etap budowania.

2. Czy aplikacja zarządza złożonym stanem po stronie klienta? Jeśli przeciąganie i upuszczanie, współpraca w czasie rzeczywistym lub obsługa danych w pierwszej kolejności offline są podstawowymi funkcjami, a nie jedynie dodatkami, złożoność frameworka takiego jak React lub Svelte jest uzasadniona. Należy dodać etap budowania.

3. Czy wiele produktów korzysta ze wspólnej biblioteki komponentów? Jeśli tak, biblioteka ta wymaga pakowania za pomocą npm, wersjonowania semantycznego i tree shaking. Należy dodać etap budowania.

4. Czy aplikacja zależy od bibliotek ekosystemu npm, które zakładają użycie bundlera? Jeśli Radix, Framer Motion, TanStack Query lub podobne biblioteki stanowią podstawę produktu, proces budowania jest niezbędny.

Jeśli odpowiedź na wszystkie cztery pytania brzmi „nie”, podejście bez etapu budowania jest dobrym rozwiązaniem. Jeśli choć jedna odpowiedź brzmi „tak”, narzędzia do budowania rozwiązują rzeczywisty problem. Błędem jest dodawanie ich, gdy wszystkie cztery odpowiedzi brzmią „nie” — oznacza to rozwiązywanie nieistniejących problemów przy jednoczesnym tworzeniu rzeczywistego narzutu związanego z zarządzaniem zależnościami.1

Porównanie stosów technologicznych

Kategoria Bez etapu budowania (ten przewodnik) React + narzędzia do budowania
Najlepsze zastosowanie Serwisy z treścią, portfolio, narzędzia wewnętrzne, aplikacje CRUD Produkty SaaS, złożone SPA, rozwiązania korzystające z systemów projektowych
Wielkość zespołu 1–5 programistów 5–50+ programistów
Zarządzanie stanem Serwer (HTMX) + klient (Alpine.js) Klient (stan React, Redux, Zustand)
Bezpieczeństwo typów W czasie działania (Pydantic po stronie serwera) Podczas kompilacji (TypeScript)
Ponowne wykorzystanie komponentów Dołączane fragmenty + makra Jinja2 Pakiety npm, współdzielone biblioteki
SEO Domyślnie renderowane po stronie serwera Wymaga konfiguracji SSR/SSG
Minimalna wydajność Wysoka (minimum JS, renderowanie po stronie serwera) Zmienna (narzut frameworka)
Górna granica złożoności Niższa (brak trybu offline i rozbudowanego stanu klienta) Wyższa (możliwa dowolna interakcja po stronie klienta)
Zależności 17 pakietów Python Ponad 300 pakietów npm
Czas budowania 0 sekund 15–60 sekund

Kiedy HTMX nie jest właściwym rozwiązaniem

HTMX zastępuje stan klienta komunikacją z serwerem. Działa to, dopóki opóźnienie nie zaczyna mieć znaczenia:

  • Interfejsy z funkcją przeciągania i upuszczania — 200 ms komunikacji z serwerem dla każdego zdarzenia przeciągania jest nieakceptowalne
  • Współpraca w czasie rzeczywistym — stan obsługiwany przez WebSocket wymaga rozwiązywania konfliktów po stronie klienta
  • Aplikacje działające w pierwszej kolejności offline — bez serwera nie ma HTMX
  • Złożone animacje powiązane ze stanem — Framer Motion i React Spring zakładają model uzgadniania React
  • Aplikacje Canvas/WebGL — pętla renderowania z natury działa po stronie klienta

W tych zastosowaniach framework działający po stronie klienta jest właściwym narzędziem. Podejście bez etapu budowania nie próbuje go zastępować.


Skrócona karta referencyjna

FastAPI

# Development
source venv/bin/activate
uvicorn app.main:app --reload --port 8000

# Production
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}

# Testing
python -m pytest -v --cov=app

# Database migrations
alembic upgrade head
alembic revision --autogenerate -m "description"

Atrybuty HTMX

hx-get="/url"                     <!-- GET request -->
hx-post="/url"                    <!-- POST request -->
hx-target="#element"              <!-- Where to put response -->
hx-swap="innerHTML"               <!-- How to insert (innerHTML, outerHTML, beforeend) -->
hx-trigger="click"                <!-- What triggers request -->
hx-trigger="keyup changed delay:300ms"  <!-- Debounced input -->
hx-trigger="load"                 <!-- Fire on element load -->
hx-indicator="#spinner"           <!-- Show during request -->
hx-push-url="true"                <!-- Update browser URL -->
hx-replace-url="true"             <!-- Replace URL (no history) -->

Atrybuty Alpine.js

x-data="{ open: false }"         <!-- Component scope + state -->
x-show="open"                    <!-- Toggle visibility -->
x-cloak                          <!-- Hide until Alpine inits -->
@click="open = !open"            <!-- Event handler -->
@click.away="open = false"       <!-- Outside click -->
@keydown.escape="open = false"   <!-- Keyboard event -->
:class="{ 'active': open }"      <!-- Dynamic class -->
:aria-expanded="open"            <!-- Dynamic attribute -->
x-text="count"                   <!-- Dynamic text content -->
x-init="fetchData()"             <!-- Run on init -->

Właściwości niestandardowe CSS

:root {
  --color-bg:     #000000;
  --color-text:   #ffffff;
  --spacing-sm:   1rem;
  --spacing-md:   1.5rem;
  --font-size-lg: 1.25rem;
}
@media (max-width: 768px) {
  :root { --gutter: 24px; }
}

Nagłówki bezpieczeństwa

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

Lista kontrolna konfiguracji projektu

[ ] FastAPI app with Jinja2Templates
[ ] Security headers middleware (CSP, HSTS, X-Frame-Options)
[ ] CSRF token generation and validation
[ ] GZip middleware (minimum_size=500)
[ ] Content-hash asset versioning (cache busting)
[ ] HTMX self-hosted in /static/js/vendor/
[ ] Alpine.js self-hosted in /static/js/vendor/
[ ] CSS custom properties for design tokens
[ ] Health check endpoint (/health)
[ ] Error handlers (404, 500)
[ ] robots.txt, sitemap.xml, llms.txt
[ ] JSON-LD structured data in base template
[ ] Hreflang tags for i18n (if multi-language)
[ ] HTML sanitization filter (nh3)
[ ] Rate limiting middleware
[ ] Deferred script loading

Często zadawane pytania

Czy HTMX nadaje się do użycia produkcyjnego w rzeczywistych aplikacjach internetowych?

Tak. HTMX pozostaje stabilny od 2020 roku i jest używany produkcyjnie w wielu branżach. Carson Gross, twórca biblioteki, traktuje zgodność wsteczną jako jedną z podstawowych zasad projektowych — dokumentacja HTMX stanowi, że biblioteka nie spowoduje niezgodności z istniejącymi aplikacjami w ramach tej samej wersji głównej.19 Biblioteka zajmuje około 16 KB po minifikacji i kompresji gzip, nie ma żadnych zależności i stosuje wersjonowanie semantyczne. Serwis blakecrosley.com korzysta z HTMX produkcyjnie od 3 lat i w tym czasie nie wystąpił żaden błąd związany z HTMX.20

Czy można używać TypeScript bez etapu kompilacji?

Częściowo. Pliki TypeScript można sprawdzać pod kątem typów za pomocą tsc --noEmit bez generowania plików wyjściowych, dzięki czemu kontrola na etapie kompilacji działa podobnie do lintera. Przeglądarki nie mogą jednak bezpośrednio wykonywać plików .ts, dlatego do udostępniania TypeScript nadal potrzebny jest etap kompilacji. Alternatywą są adnotacje typów JSDoc w zwykłych plikach .js, które TypeScript może sprawdzać bez kompilacji. Zapewnia to bezpieczeństwo typów podczas programowania, a jednocześnie pozwala dostarczać standardowy JavaScript.

Jak to podejście wypada na tle Astro lub 11ty?

Astro i 11ty to generatory witryn statycznych, które tworzą zwykły HTML z minimalną ilością JavaScript po stronie klienta, ale wymagają etapu kompilacji (Node.js, npm install i polecenia kompilacji). Podejście bez kompilacji eliminuje ten etap — serwer renderuje HTML przy każdym żądaniu. Wiąże się to z kompromisem: Astro/11ty tworzą szybsze strony statyczne (bez obliczeń po stronie serwera), natomiast FastAPI + HTMX natywnie obsługuje treści dynamiczne (dane właściwe dla użytkownika, przesyłanie formularzy i aktualizacje w czasie rzeczywistym) bez osobnej warstwy API.

A co z renderowaniem po stronie serwera (SSR) w React?

SSR w Next.js oraz podejście FastAPI + HTMX mają wspólny cel: przesyłanie do przeglądarki kodu HTML wyrenderowanego po stronie serwera. Różnica polega na tym, co dzieje się po początkowym renderowaniu. Next.js przeprowadza hydratację strony za pomocą React, dostarczając klientowi środowisko uruchomieniowe frameworka i kod komponentów. FastAPI + HTMX nie przeprowadza hydratacji — HTML stanowi ostateczny wynik. HTMX obsługuje kolejne interakcje, żądając od serwera nowych fragmentów HTML. W efekcie FastAPI + HTMX dostarcza łącznie około 35–40 KB kodu JavaScript, podczas gdy aplikacja Next.js — od 100 do 300 KB.18

Jak obsługiwać walidację formularzy w tym stosie?

Po stronie serwera. Pydantic sprawdza dane wejściowe po przesłaniu formularza. Jeśli walidacja zakończy się niepowodzeniem, serwer zwraca formularz z komunikatami o błędach. HTMX podmienia odpowiedź w DOM:

<form hx-post="/contact" hx-target="#form-container" hx-swap="outerHTML">
  <input type="email" name="email" required>
  <button type="submit">Send</button>
</form>
@router.post("/contact")
async def contact(request: Request, email: str = Form(...)):
    if not validate_email(email):
        return templates.TemplateResponse("components/_contact_form.html", {
            "request": request,
            "error": "Please enter a valid email address",
            "email": email,  # Preserve input
        })
    await send_email(email)
    return templates.TemplateResponse("components/_contact_success.html", {
        "request": request
    })

Serwer przeprowadza walidację i renderuje stany błędów, a HTMX podmienia wynik. Biblioteka do walidacji po stronie klienta nie jest potrzebna. Atrybut HTML required zapewnia podstawową walidację na poziomie przeglądarki jako pierwszą linię ochrony.

Czy można dodać funkcje czasu rzeczywistego (WebSockets)?

Tak. FastAPI ma wbudowaną obsługę WebSocket:

from fastapi import WebSocket

@app.websocket("/ws/notifications")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await get_notification()
        await websocket.send_text(render_notification_html(data))

HTMX oferuje rozszerzenie WebSocket (hx-ws), które łączy elementy z punktami końcowymi WebSocket:

<!-- HTMX 2.x WebSocket extension syntax -->
<div hx-ext="ws" ws-connect="/ws/notifications">
  <div id="notifications" ws-send></div>
</div>

Uwaga: HTMX 1.x korzystał ze składni hx-ws="connect:...". W HTMX 2.x obsługę WebSocket przeniesiono do osobnego rozszerzenia (htmx-ext-ws) z przedstawionymi powyżej atrybutami ws-connect i ws-send. W przypadku korzystania z HTMX 1.x stara składnia hx-ws nadal działa.

Wersja beta HTMX 4.0: htmx 4.0.0-beta6 jest już dostępny w npm pod tagiem next, podobnie jak dokumentacja wersji 4.0 (beta6 opublikowano 23 lipca 2026), podczas gdy szybki start na htmx.org i tag npm latest nadal dotyczą wersji 2.0.10. Ten przewodnik wciąż jest przeznaczony dla HTMX 2.x, który pozostaje zalecaną wersją do zastosowań produkcyjnych do czasu ustabilizowania się wersji 4.0; migracja z 2.x do 4.x stanowi zmianę generacyjną, a nie aktualizację punktową wersji 2.x. Schemat wersjonowania big-skies-software pomija nieparzyste wersje główne, dlatego 4.0 jest kolejnym krokiem po 2.x.2122

Co warto obserwować w dokumentacji wersji 4.0. Przed wydaniem 4.0 GA na szczególną uwagę podczas przeglądu bezpieczeństwa i architektury zasługują 2 nowości: nowe rozszerzenie hx-live wprowadza reaktywne względem DOM wyrażenia, które są ponownie obliczane po zmianie stanu, do którego się odwołują, natomiast nowe rozszerzenie hx-nonce uzależnia przetwarzanie atrybutów htmx od nonce CSP. Przewodnik migracji do wersji 4.0 przenosi także kilka koncepcji konfiguracyjnych, przywraca lub zmienia część zachowań związanych ze zdarzeniami i historią oraz usuwa z rdzenia niektóre funkcje pomocnicze JavaScript. Wersję 4.0 należy traktować jako projekt migracyjny, a nie bezpośrednią aktualizację 2.x.21

Wiadomości z serwera są podmieniane w DOM przy użyciu tych samych mechanizmów wskazywania celu i podmiany co odpowiedzi HTTP. Serwer przesyła fragmenty HTML przez WebSocket, a HTMX je wstawia.

Jak ten stos obsługuje SEO?

Renderowany po stronie serwera HTML jest z natury przyjazny dla SEO, ponieważ roboty indeksujące otrzymują pełną treść strony bez wykonywania JavaScript. Serwis blakecrosley.com dodaje kilka warstw SEO:

  • Dane strukturalne JSON-LD w <head> każdej strony (schematy Person, Article, WebSite i FAQPage)
  • Dynamiczna mapa witryny z alternatywnymi adresami hreflang dla wszystkich 10 wersji językowych
  • Kanał RSS pod adresem /blog/feed.xml
  • llms.txt w katalogu głównym, ułatwiający robotom AI wykrywanie treści
  • Kanoniczne adresy URL i tagi Open Graph w szablonie bazowym
  • Semantyczny HTML: <article>, <section>, <main> oraz prawidłowa hierarchia nagłówków

Konfiguracja SSR nie jest potrzebna. Nie ma getStaticProps. Nie ma ISR. HTML jest renderowany przy każdym żądaniu — to zachowanie domyślne, a nie optymalizacja.

Jak wygląda krzywa uczenia się w porównaniu z React?

Dla programistów Python krzywa uczenia się jest znacznie łagodniejsza. Język jest już znany. Procedury obsługi tras w FastAPI zwracają odpowiedzi z szablonów — to ten sam model mentalny co widoki Flask lub Django. HTMX dodaje kilka atrybutów HTML (hx-get, hx-target, hx-swap). Alpine.js dodaje jeszcze kilka (x-data, x-show, @click). Nie trzeba poznawać JSX, wirtualnego DOM, systemu hooków, biblioteki do zarządzania stanem ani konfiguracji narzędzi do kompilacji.

Dokumentacja HTMX mieści się na jednej długiej stronie. Dokumentacja Alpine.js zajmuje kilka stron. Dokumentacja React obejmuje setki stron poświęconych hookom, kontekstowi, referencjom, efektom, Suspense, komponentom serwerowym i strumieniowemu SSR.

Dla programistów JavaScript/React zmiana ma charakter bardziej koncepcyjny niż składniowy. Kluczowe jest zrozumienie, że stan należy do serwera i to serwer renderuje HTML. Zarządzanie stanem po stronie klienta staje się obsługą tras po stronie serwera. Pobieranie danych po stronie klienta zastępują atrybuty HTMX w elementach HTML. Składnia jest prostsza — model mentalny wymaga jednak odejścia od założenia właściwego aplikacjom SPA, że za renderowanie odpowiada klient.


Dziennik zmian

Data Zmiana Źródło
2026-08-16 Starlette 1.3.1 → 1.6.0, a jedna z jego zmian po cichu zmienia własny fragment GZip w tym przewodniku. Zmiana zachowania: Starlette 1.5.0 (8 sierpnia) znacznie rozszerzył DEFAULT_EXCLUDED_CONTENT_TYPES poza text/event-stream, obejmując archiwa gzip/zip, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/* oraz fonty WOFF/WOFF2, więc domyślnie nie są już kompresowane; image/* celowo nie został wykluczony, dzięki czemu image/svg+xml nadal może być kompresowany. Nowy argument wyłącznie słów kluczowych exclude_content_types zastępuje tę listę, dopasowanie nie rozróżnia wielkości liter, a ponowne przypisanie stałej modułu w czasie działania przestało działać. Wymaganie wykonawcze FastAPI to starlette>=0.46.0 bez górnej granicy, więc świeża instalacja pobiera 1.6.0, a zmiana trafia do fragmentu w tym przewodniku bez żadnego działania ze strony czytelnika — poprawiono oba fragmenty dotyczące GZip. Wersja 1.5.0 pomija również częściowe odpowiedzi ze statusem 206 i opróżnia bufor dla każdego przesyłanego fragmentu; 1.4.0 (5 sierpnia) przeniósł fragmenty gzip o rozmiarze co najmniej 128 KiB thread_minimum_size do wątku roboczego, aby duże kompresje nie blokowały pętli zdarzeń. Nowa funkcja: 1.6.0 (8 sierpnia) dodał max_body_size w Starlette/Router/Mount/Route oraz RequestBodyLimitMiddleware — obejmuje to nowy podrozdział Security, ponieważ ograniczanie rozmiaru treści żądania było całkowicie nieobecne w tym przewodniku. Odnotowano również: encode/starlette przekierowuje teraz do Kludex/starlette, na wzór zmiany w Uvicorn. Wyłącznie w dzienniku zmian: Uvicorn 0.52.0–0.52.3 (eksperymentalna implementacja HTTP/1.1 zttp oparta na Zig, której samo wydanie nie zaleca umieszczać przed ruchem produkcyjnym; zalecenie przewodnika dotyczące --http httptools pozostaje aktualne), Alpine.js 3.16.0/3.16.1, SQLAlchemy 2.0.52 (obsługa Python 3.15; poprawka niezgodności kolumn wyniku ORM UPDATE synchronize_session="fetch"). Zweryfikowano bez zmian: FastAPI 0.141.1, HTMX 2.0.10 latest / 4.0.0-beta6 next, Bootstrap 5.3.8, Jinja2 3.1.6, Pydantic 2.13.4 stable. W tym okresie nie pojawiły się żadne nowe zalecenia bezpieczeństwa dla wszystkich dziewięciu zależności. 28
2026-07-29 FastAPI 0.141.0 + 0.141.1 (obie wersje 29 lipca). Wersja 0.141.0 dodaje app.frontend(check_dir="auto"), dzięki czemu fastapi dev nie kończy się już błędem, gdy brakuje katalogu kompilacji — co jest typową sytuacją podczas uruchamiania serwera przed wykonaniem kompilacji frontendu. Wersja 0.141.1, wydana kilka godzin później, naprawia problem polegający na tym, że zależności w app.frontend() odrzucały zadania w tle i nagłówki odpowiedzi; zależność ustawiająca plik cookie lub planująca BackgroundTask miała tę pracę porzucaną na montowaniu frontendu, choć na trasach API działała poprawnie, więc zamyka to rzeczywistą lukę w obsłudze zależności dodanej w 0.139.0. Obie zmiany trafiają do istniejącej narracji o app.frontend(), a nie do nowej sekcji, ponieważ podejście tego przewodnika oparte na renderowaniu po stronie serwera nie montuje katalogu dist/. Wersja 0.141.1 dokumentuje też FASTAPI_ENV w przewodniku FastAPI CLI (tylko dokumentacja, bez zmiany treści). 29
2026-07-27 FastAPI wydał wersje od 0.140.1 do 0.140.7 w ciągu pięciu i pół godziny 27 lipca — siedem wydań, wszystkie będące refaktoryzacjami mechanizmu zależności rozpoczętego w 0.140.0. Dwa wątki: spłaszczona kopia grafu zależności, którą FastAPI tworzył i przechowywał, została usunięta (0.140.2), podobnie jak wszystkie pozostałe miejsca, które ją przebudowywały — generowanie OpenAPI (0.140.3, 0.140.7), pola treści (0.140.5), parametry żądania (0.140.6) — a wersja 0.140.4 usuwa nieodczytywane księgowanie śledzenia powtórzeń. Jedyną zmianą z widocznym progiem jest 0.140.1: lru_cache w pomocniczych funkcjach klasyfikacji wywoływalnych w fastapi/dependencies/models.py zwiększono z 1 024 do 4 096 wpisów (o nazwie _CALLABLE_CLASSIFICATION_CACHE_SIZE) po zgłoszeniach aplikacji przekraczających 1 024 różne zależności, które powodowały ciągłe opróżnianie pamięci podręcznej. Brak zmian w API; zalecenie w akapicie o pamięci zależności przesuwa się z 0.140.0 na 0.140.7 lub nowszą, z uwagą, że ta gałąź nadal się rozwija, a benchmarki zależności OpenAPI (PR #16075) pojawiły się dopiero w ostatnim wydaniu tej serii. 30
2026-07-25 FastAPI 0.140.0 (24 lipca, 21:16 UTC) naprawia regresję pamięci systemu zależności, obecną od 0.121.0 (3 listopada 2025). PR #16049 usuwa dziesięć atrybutów functools.cached_property z Dependant, przenosi je do pomocniczych funkcji na poziomie modułu i czyni klasę @dataclass(slots=True); oficjalne uruchomienie CodSpeed dla scalonego PR raportuje, że benchmark pamięci test_dependency_graph zmalał z 17,5 MB do 1,1 MB (×16), a pierwotne zgłoszenie opisywało produkcyjny OOM w 0.121.3, podczas gdy 0.120.4 utrzymywał zużycie poniżej ~400 MB. Dodano fragment dotyczący 0.140.0 do Async Patterns oraz wiersz o pamięci workerów do Uvicorn Production Configuration. Poprawiono również istniejący błąd: przewodnik twierdził, że 0.137.0 „przypina Starlette do linii 1.x” — nie jest to prawdą. Wymaganie wykonawcze FastAPI to starlette>=0.46.0 (dolna granica bez górnego limitu, nadal spełniana przez Starlette 0.4x) jednolicie w 0.136.3, 0.137.0, 0.138.0, 0.139.2 i 0.140.0; numery 1.x w informacjach o 0.137.0 to aktualizacje dependabot dla pliku blokady testów repozytorium (PR #15722 dotyka wyłącznie uv.lock). Poprawiono zarówno twierdzenie w treści, jak i 24. Dwie oznaczone zmiany bez wpływu: elementy wewnętrzne Dependant są teraz niekompatybilne dla narzędzi (oauth_scopes, cache_key, _uses_scopes, _is_security_scheme zniknęły jako atrybuty, zastąpione przez funkcje modułu _get_oauth_scopes() / _get_cache_key() / _uses_scopes(), a slots=True blokuje monkey-patching instancji) — nieudokumentowane wewnętrzne API, do których ten przewodnik nigdy się nie odwołuje, w tej samej kategorii co zmiana router.routes w 0.137.0; oraz oficjalna dokumentacja FastAPI domyślnie używa teraz projektów uv zamiast pip/venv w 30 plikach, w tym README, index.md, virtual-environments.md i na stronach Docker/deployment (PR #16032, scalony 21 lipca). Zmiana dokumentacji jest kosmetyczna dla kodu, lecz przewodnik wszędzie uczy pip install -r requirements.txt i obecnie odbiega od głównego wejścia upstreamu — to przyszła decyzja redakcyjna, celowo niepodejmowana w tym przebiegu. 27
2026-07-24 htmx 4.0.0-beta6 zastępuje beta5 jako tag npm next (opublikowano 23 lipca 2026; wydanie GitHub tego samego dnia). Najważniejsze elementy beta: nowe rozszerzenie hx-multipart (strumieniowane odpowiedzi multipart/mixed/multipart/parallel z nagłówkami akcji HX-* dla każdej części), przywracanie przewijania historii przez Navigation API z obejściem dla Firefoxa, zmiana nazwy zdarzenia wewnętrznego beta htmx:swap:finallyhtmx:finally:swap, zdarzenia nagłówka odpowiedzi HX-Trigger są teraz uruchamiane po swapie, niestandardowe metody żądań oraz przepisanie hx-ws z przekazywaniem protocols. Zalecenie pozostaje bez zmian — produkcja używa HTMX 2.x (latest = 2.0.10) do czasu 4.0 GA; zmiana nazwy jest niezgodna wyłącznie w linii beta 4.0. Zaktualizowano uwagę o ścieżce beta i 21. Zweryfikowano bez zmian: FastAPI 0.139.2, Uvicorn 0.51.0, Alpine.js 3.15.12, Starlette 1.3.1, Jinja2 3.1.6; w tym okresie nie pojawiły się żadne zalecenia bezpieczeństwa.
2026-07-17 FastAPI 0.139.1 + 0.139.2 (16 lipca): poprawka ścieżek kropkowych dla fallbacków app.frontend() (/users/john.doe, PR #16011) oraz bezpieczne wątkowo tworzenie tras routera dla testów równoległych wątków (PR #16013) — bez zmiany API widocznej dla aplikacji. Uvicorn 0.49.0 → 0.51.0: starsza implementacja websockets została wycofana, a auto domyślnie używa teraz websockets-sansio (0.50.0), domyślna implementacja wymaga websockets>=13.0 (0.50.2), a 0.51.0 (8 lipca) dodaje restarty workerów SIGHUP z nakładaniem się procesów dla niemal zerowego czasu przestoju; repozytorium znajduje się teraz pod Kludex/uvicorn. HTMX (2.0.10 / 4.0.0-beta5 next), Alpine.js 3.15.12, Starlette 1.3.1, Pydantic 2.13.4, SQLAlchemy 2.0.51, Bootstrap 5.3.8 — wszystkie zweryfikowano bez zmian; w tym okresie nie pojawiły się żadne zalecenia bezpieczeństwa.
2026-07-07 htmx 4.0.0-beta5 jest teraz tagiem npm next (opublikowano 26 czerwca 2026), zastępując beta4; zaktualizowano uwagę o ścieżce beta HTMX 4.0 oraz [^22], aby to odzwierciedlić. Zalecenie pozostaje bez zmian — prace produkcyjne pozostają przy HTMX 2.x (latest = 2.0.10) do czasu 4.0 GA. Zweryfikowano względem tagów npm dist htmx.org.
2026-07-02 FastAPI 0.139.0 (1 lipca). app.frontend() obsługuje teraz dependencies — na przykład automatyczne uwierzytelnianie plikiem cookie dla obsługiwanego frontendu (PR #15908) — rozszerzając montowanie statycznego frontendu z 0.138.0 o standardowy mechanizm Depends(); nadal jest to niezależne od tezy tego przewodnika o renderowaniu po stronie serwera, co odnotowano w tym samym akapicie porównawczym. Brak innych zmian w stosie: HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8, SQLAlchemy 2.0.51 bez zmian. 26
2026-06-22 FastAPI 0.138.0 + 0.137.2. Wersja 0.138.0 (20 czerwca) dodaje app.frontend("/", directory="dist") / router.frontend(...) do obsługi skompilowanego statycznego frontendu (wynik SPA dist/) — niezależne od tezy tego przewodnika o renderowaniu po stronie serwera bez procesu kompilacji, odnotowane jako kontrast w sekcji Async Patterns. Wersja 0.137.2 (18 czerwca) dodaje iter_route_contexts() jako wspierany sposób wyliczania tras, teraz gdy router.routes jest wewnętrzne (od 0.137.0). Obie to dodatki funkcjonalne, bez zmian niezgodnych wstecznie; Starlette (1.3.1), Pydantic (2.13.4), HTMX (2.0.10), Alpine.js (3.15.12), Bootstrap (5.3.8), SQLAlchemy (2.0.51) — wszystkie bez zmian. 25
2026-06-16 FastAPI 0.137.0/0.137.1 + Starlette 1.0→1.3.1. FastAPI 0.137.0 (14 czerwca) refaktoryzuje elementy wewnętrzne routera: router.routes jest teraz wewnętrznym drzewem, a nie płaską listą APIRoute (niezgodne dla wszystkiego, co ją iteruje), jednocześnie umożliwiając trasy dodawane po include_router() oraz nowe hooki APIRouter.matches()/.handle(); 0.137.1 (15 czerwca) poprawia typowanie APIRoute i routery bez prefiksu z pustą ścieżką. Starlette wydał pierwszą stabilną wersję 1.0 (22 marca) i jest teraz w wersji 1.3.1 (12 czerwca), usuwając wycofane hooki on_event/on_startup/on_shutdown oraz dekoratory @app.route()/@app.websocket_route() — jedynymi ścieżkami są lifespan i Route/WebSocketRoute. (Ten wpis pierwotnie stwierdzał, że FastAPI 0.137.0 przypina Starlette 1.3.1 — poprawiono 2026-07-25: nie robi tego; wymaganie wykonawcze to starlette>=0.46.0 bez górnej granicy.) Dodano uwagę o lifespan/routerze do sekcji Async Patterns. SQLAlchemy 2.0.51 (15 czerwca) zawiera wyłącznie poprawki błędów. 24
2026-06-08 Zmiana instalacji async w SQLAlchemy 2.0.50. Od SQLAlchemy 2.0.50 zależność greenlet stosu async nie instaluje się już domyślnie — należy zainstalować dodatek sqlalchemy[asyncio] (w przeciwnym razie pierwsze await względem silnika zakończy się błędem braku greenlet). Wersja 2.0.50 wymaga również Python 3.10+ (usunięto 3.7–3.9) i dodaje koła dla wielowątkowego 3.13t. Dodano uwagę o instalacji do sekcji SQLAlchemy 2.0 Async. Brak zmiany treści dla pozostałej części stosu: najnowszy FastAPI nadal to 0.136.3 (2026-05-23, bez wydania w czerwcu), stabilny htmx pozostaje 2.0.10 (4.0.0-beta4 „The Fetchening” jest w becie z celem stabilnego wydania około początku 2027 r., jeszcze bez rekomendacji produkcyjnej), Alpine.js 3.15.12, Bootstrap 5.3.x bez zmian. Zalecenie produkcyjne pozostaje bez zmian: HTMX 2.x do czasu stabilnego 4.0.23
2026-05-24 Kontrola utrzymaniowa: lokalny spis treści nadal wskazuje 210 wpisów na blogu, 11 głównych przewodników, 48 studiów projektowych i 10 obsługiwanych języków, w tym angielski. Najnowszy FastAPI to 0.136.3 (2026-05-23); jedyną refaktoryzacją widoczną dla aplikacji, wskazaną w informacjach o wydaniu, jest bardziej restrykcyjna obsługa nagłówków z podkreśleniami przy convert_underscores=True, a 0.136.2 waliduje pola Server-Sent Event, aby uniknąć uszkodzonych danych zdarzeń. Stabilny htmx pozostaje 2.0.10, podczas gdy npm next i dokumentacja 4.0 wskazują teraz na 4.0.0-beta4; najnowszy SQLAlchemy 2.0 to 2.0.50; najnowszy Pydantic pozostaje 2.13.4. Zalecenie produkcyjne pozostaje bez zmian: używać HTMX 2.x, dopóki 4.0 nie stanie się stabilne.122
2026-05-18 Odświeżenie spisu witryny: lokalny spis treści wskazuje teraz 210 wpisów na blogu, 11 głównych przewodników, 48 studiów projektowych i 10 obsługiwanych języków, w tym angielski. Najnowszy FastAPI pozostaje 0.136.1; stabilny htmx pozostaje 2.0.10 z npm next na 4.0.0-beta3; najnowszy npm Alpine.js pozostaje 3.15.12. Zalecenie produkcyjne pozostaje bez zmian: używać HTMX 2.x, dopóki 4.0 nie stanie się stabilne.12021
2026-05-15 Kontrola utrzymaniowa: najnowszy FastAPI pozostaje 0.136.1; to lokalne środowisko witryny importuje FastAPI 0.128.0 i Starlette 0.50.0; stabilny htmx pozostaje 2.0.10, a npm next to teraz 4.0.0-beta3; najnowszy npm Alpine.js to 3.15.12; najnowszy Bootstrap to 5.3.8; najnowszy SQLAlchemy 2.0 to 2.0.49; najnowszy Pydantic to 2.13.4. Zalecenie produkcyjne bez zmian: używać HTMX 2.x, dopóki 4.0 nie stanie się stabilne.2021
2026-05-09 Śledzenie htmx 4.0.0-beta3 (8 maja 2026): htmx 4.0.0-beta3 jest dostępny pod tagiem npm next oraz w dokumentacji 4.0, natomiast npm latest pozostaje 2.0.10. Najważniejsze elementy warte śledzenia przed GA: nowe rozszerzenie hx-live (reaktywne wobec DOM wyrażenia), nowe rozszerzenie hx-nonce (ochrona nonce CSP dla atrybutów htmx) oraz zmiany w przewodniku migracji dotyczące konfiguracji, historii, zdarzeń i podstawowych pomocników JavaScript. Zalecenie produkcyjne pozostaje bez zmian: htmx 2.x pozostaje najnowszym tagiem npm i zalecaną wersją do czasu 4.0 GA.21
2026-05-07 Kontrola utrzymaniowa: najnowszy FastAPI pozostaje 0.136.1; stabilny htmx to 2.0.10, a v4 nadal jest w becie z celem na lato ’26; najnowszy npm Alpine.js to 3.15.12; najnowszy Bootstrap to 5.3.8; najnowszy SQLAlchemy 2.0 to 2.0.49; najnowszy Pydantic to 2.13.4. Lokalne wskaźniki witryny odświeżono do 182 wpisów na blogu, 11 przewodników, dziesięciu obsługiwanych języków i 17 wymagań Python. Wytyczne migracyjne bez zmian: używać HTMX 2.x w produkcji, dopóki 4.0 nie stanie się stabilne.20
2026-04-25 FastAPI 0.136.1 (23 kwietnia 2026): porządki związane z wycofywaniem Pydantic v2 (bez zmian zachowania kodu aplikacji). Śledzenie harmonogramu HTMX 4.0: wydano htmx 4.0.0-beta1 (6 kwietnia) i 4.0.0-beta2 (14 kwietnia). Wytyczne migracyjne bez zmian — htmx 2.x pozostaje pod najnowszym tagiem npm, dopóki 4.0 nie będzie stabilne; poprawki bezpieczeństwa są kontynuowane, bez presji na aktualizację. Główne zmiany 4.0, które już teraz warto uwzględnić w projekcie: (1) fetch() zastępuje XMLHttpRequest jako podstawową infrastrukturę ajax, (2) dziedziczenie atrybutów domyślnie staje się jawne, (3) obsługa historii wysyła żądanie sieciowe po przywróconą treść (bez lokalnego zrzutu DOM). FastAPI 0.135.4 (16 kwietnia) usunął primaaprilisowy dekorator @app.vibe(), który pojawił się w 0.135.3.
2026-04-16 Dodano świadomość HTMX 4.0-beta (odwołanie przyszłościowe). Odnotowano obsługę FastAPI 0.136.0 dla wielowątkowych kompilacji Python 3.14t. Funkcje Pydantic 2.13.x (fabryki wartości domyślnych atrybutów prywatnych z dostępem do zwalidowanych danych modelu, przestrzeń nazw pydantic.v1 do 1.10.26 z obsługą 3.14). Poprawki Alpine.js 3.15.11: modyfikator x-anchor.noflip, ostrzeżenie x-for o wielu elementach głównych, poprawka regresji morph w $refs.
2026-03-24 Pierwsza publikacja

Źródła


Ten przewodnik obejmuje kompletny system używany do tworzenia blakecrosley.com. No-Build Manifesto przedstawia argument filozoficzny. Wpis Lighthouse Perfect Score dokumentuje proces optymalizacji wydajności. Wpis Vibe Coding vs. Engineering analizuje, gdzie w tym procesie mieści się rozwój wspomagany przez AI.


  1. metryki produkcyjne blakecrosley.com na dzień 18 maja 2026 r. Witryna ma 210 wpisów na blogu, interaktywne komponenty JavaScript, 11 głównych przewodników, 48 studiów projektowych, angielski oraz 9 przetłumaczonych wersji językowych, minimalne zależności Python i zero narzędzi do budowania. Zweryfikowano na podstawie lokalnego spisu treści, app/i18n/config.py i requirements.txt

  2. Google PageSpeed Insights (pagespeed.web.dev) uruchamia audyty Lighthouse dla każdego publicznego URL-a. blakecrosley.com osiąga wynik 100/100/100/100 (wydajność, dostępność, sprawdzone metody, SEO) na marzec 2026 r. Wyniki można zweryfikować publicznie. Pełny opis procesu optymalizacji znajduje się w artykule From 76 to 100: Achieving a Perfect Lighthouse Score

  3. Świeże npx create-next-app@latest (Next.js 15, testowane w lutym 2026 r.) instaluje 311 pakietów w node_modules/ o łącznym rozmiarze 187 MB. Projekty produkcyjne z dodatkowymi zależnościami zwykle zajmują więcej. Poszczególne projekty się różnią. Źródło: testy autora udokumentowane w The No-Build Manifesto

  4. Dokumentacja wydajnościowa Next.js firmy Vercel zaleca konkretne optymalizacje (optymalizację obrazów, ładowanie fontów, dzielenie kodu), aby osiągać wyniki powyżej 90. Zob. nextjs.org/docs/app/building-your-application/optimizing. Zakres 70–90 odzwierciedla ustawienia domyślne przed zastosowaniem tych optymalizacji. 

  5. Pełną listę zależności zweryfikowano na podstawie requirements.txt blakecrosley.com na maj 2026 r. Plik zawiera obecnie 17 wpisów wymagań Python oraz zero narzędzi do budowania, kompilatorów i bundlerów. 

  6. Na podstawie doświadczeń autora z utrzymywaniem projektów Next.js (2021–2024), ekosystem JavaScript generuje dla aktywnych projektów 15–25 pull requestów Dependabot miesięcznie, z których większość aktualizuje przechodnie zależności, których deweloper nigdy bezpośrednio nie zaimportował. 

  7. Tim Berners-Lee sformułował zgodność wsteczną jako zasadę projektowania sieci: „przeglądarka powinna być zgodna wstecznie”. Strona z 1996 r. renderuje się w Chrome 2026. Zob. w3.org/DesignIssues/Principles

  8. OWASP zaleca wyłączenie endpointów dokumentacji API w środowisku produkcyjnym, aby ograniczyć powierzchnię ataku. Endpoint /openapi.json udostępnia wszystkie definicje tras, parametry i modele odpowiedzi. 

  9. Dokumentacja FastAPI dotycząca handlerów async i sync: fastapi.tiangolo.com/async/. Łączenie await z blokującymi wywołaniami w funkcjach async zagładza pętlę zdarzeń. 

  10. nh3 to oparty na Rust sanitizer HTML, następca biblioteki Bleach. Jest utrzymywany przez projekt PyO3 i zapewnia sanitizację HTML opartą na liście dozwolonych elementów. Zob. github.com/messense/nh3

  11. Nagłówek Vary jest zdefiniowany w RFC 9110, sekcja 12.5.5. Nakazuje pamięciom podręcznym przechowywać osobne odpowiedzi zależnie od wartości wskazanych nagłówków żądania. Bez Vary: HX-Request CDN mógłby zwrócić fragment HTMX jako odpowiedź pełnej strony. Zob. httpwg.org/specs/rfc9110.html#field.vary

  12. Własne właściwości CSS (zmienne CSS) są obsługiwane przez ponad 97% globalnych przeglądarek. Kaskadują, są dziedziczone i reagują na media queries w czasie działania — możliwości, których nie mają zmienne preprocesora. Źródło: caniuse.com/css-variables

  13. Dokumentacja hreflang Google: developers.google.com/search/docs/specialty/international/localized-versions. Wartość x-default wskazuje stronę zastępczą dla użytkowników, których języka nie ma na liście hreflang. 

  14. Alpine.js wymaga 'unsafe-eval' w Content Security Policy dla swojego silnika ewaluacji wyrażeń. Wersja zgodna z CSP (@alpinejs/csp) pozwala uniknąć tego wymogu, ale ma ograniczenia. Zob. alpinejs.dev/advanced/csp

  15. Tokeny CSRF oparte na HMAC stosują wzorzec „Signed Double-Submit Cookie” opisany w OWASP CSRF Prevention Cheat Sheet. hmac.compare_digest używa porównania w stałym czasie, aby zapobiegać atakom kanałem bocznym opartym na czasie. Zob. cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

  16. WebP zapewnia pliki o 25–35% mniejsze niż JPEG przy równoważnej jakości wizualnej. Badanie WebP Google: developers.google.com/speed/webp/docs/webp_study

  17. 103 Early Hints pozwala serwerowi (lub CDN) wysłać wstępną odpowiedź ze wskazówkami preload, zanim będzie gotowa odpowiedź końcowa. Cloudflare obsługuje Early Hints dla nagłówków Link z rel=preload. Zob. developer.chrome.com/blog/early-hints

  18. React 18 + ReactDOM ważą około 42 KB po minifikacji i kompresji gzip. Z routerem, biblioteką zarządzania stanem i runtime frameworka typowe aplikacje React dostarczają 100–300 KB frameworkowego JavaScript. Źródło: bundlephobia.com/package/react-dom@18.2.0

  19. Polityka wersjonowania HTMX i zobowiązanie do zgodności wstecznej są udokumentowane pod adresem htmx.org/migration-guide-htmx-1/. Carson Gross przedstawił zasadę zgodności wstecznej w Hypermedia Systems (2023) autorstwa Grossa, Stepinskiego i Cottera: hypermedia.systems

  20. Kontrola utrzymaniowa z 15 maja 2026 r. FastAPI PyPI i informacje o wydaniach wymieniają 0.136.1; lokalna weryfikacja importu zwróciła FastAPI 0.128.0 i Starlette 0.50.0 dla środowiska tej witryny; htmx.org podaje 2.0.10 w przewodniku szybkiego startu; npm view htmx.org version dist-tags zwróciło latest=2.0.10 i next=4.0.0-beta3; npm view alpinejs version i npm view @alpinejs/csp version zwróciły 3.15.12; oficjalny blog Bootstrap oraz metadane pakietu npm wymieniają 5.3.8; SQLAlchemy PyPI i dokumentacja wymieniają 2.0.49; Pydantic PyPI wymienia 2.13.4. 

  21. htmx 4.0.0-beta6 jest bieżącym tagiem npm next (opublikowanym 23 lipca 2026 r.; linia beta przeszła od beta3 8 maja 2026 r. → beta4 → beta5 → beta6), podczas gdy npm latest nadal wskazuje 2.0.10. Dokumentacja 4.0 na four.htmx.org śledzi build next, indeks rozszerzeń 4.0 wymienia hx-live i hx-nonce, a przewodnik migracji 4.0 dokumentuje zmiany migracyjne, które należy przejrzeć przed przeniesieniem aplikacji produkcyjnych z wersji 2.x. Zweryfikowano względem tagów dystrybucji npm htmx.org 24 lipca 2026 r. 

  22. Kontrola utrzymaniowa z 24 maja 2026 r. Lokalne polecenia inwentaryzacyjne zwróciły 210 wpisów blogowych Markdown, 11 plików przewodników najwyższego poziomu i 48 plików studiów projektowych. FastAPI informacje o wydaniach wymieniają 0.136.3 z 2026-05-23 z bardziej rygorystyczną obsługą nagłówków z podkreśleniem, gdy convert_underscores=True; 0.136.2 waliduje pola Server-Sent Event. python3 -m pip index versions fastapi zwróciło najnowszą wersję 0.136.3; python3 -m pip index versions sqlalchemy zwróciło najnowszą wersję 2.0.50; python3 -m pip index versions pydantic zwróciło najnowszą wersję 2.13.4. npm view htmx.org dist-tags version time.modified --json zwróciło latest=2.0.10, next=4.0.0-beta4 i time.modified=2026-05-22T15:56:21.948Z; dokumentacja instalacji four.htmx.org pokazuje htmx.org@4.0.0-beta4

  23. Dziennik zmian SQLAlchemy 2.0.50 i blog wydania, wydane 2026-05-24. Zależność asyncio greenlet nie instaluje się już domyślnie; aby ją pobrać, wymagany jest teraz cel instalacji sqlalchemy[asyncio]. Wersja 2.0.50 porzuca również obsługę Python 3.7/3.8/3.9 (obecnie 3.10+), dodaje wolne od GIL koła Python i dodaje parametr ramki okna over(..., exclude=...). Najnowszą wersję zweryfikowano w PyPI na 2026-06-08. htmx 4.0.0-beta4 („The Fetchening”, 2026-05-22) pozostaje wersją beta, z celem stabilnego wydania na początek 2027 r.; FastAPI 0.136.3 (2026-05-23), Alpine.js 3.15.12 i Bootstrap 5.3.x pozostają bez zmian w tym okresie. 

  24. FastAPI informacje o wydaniach: 0.137.0 (2026-06-14) refaktoryzuje wewnętrzne elementy routera, przez co router.routes nie jest już płaską listą obiektów APIRoute, lecz drzewem obiektów pośrednich (należy traktować je jako wewnętrzne); umożliwia również dodawanie tras po include_router(), w tym pod-routera przed zdefiniowaniem jego tras, unika kopiowania tras i dodaje APIRouter.matches()/.handle(). Nie przypina Starlette do wersji 1.x: wymaganie wykonawcze FastAPI to starlette>=0.46.0 — dolna granica bez górnego limitu — identycznie w wersjach 0.136.3, 0.137.0, 0.138.0, 0.139.2 i 0.140.0, co zweryfikowano względem metadanych requires_dist w PyPI JSON API 2026-07-25. Wiersz „bump starlette from 1.1.0 to 1.2.1” (PR #15722) w informacjach 0.137.0 jest aktualizacją dependabot w sekcji Internal, która dotyka wyłącznie pliku blokady testów uv.lock repozytorium. (Górna granica istniała wcześniej — 0.120.4 i 0.121.0 dostarczano z starlette<0.50.0,>=0.40.0 — ale została usunięta w 0.136.3.) Korektę zastosowano 2026-07-25; wcześniejsze brzmienie tego przypisu i twierdzenie w treści były błędne. Wersja 0.137.1 (2026-06-15) naprawia typowanie APIRoute oraz pustą ścieżkę w routerze bez prefiksu. Starlette informacje o wydaniach: 1.0.0 (2026-03-22), pierwsze stabilne wydanie od około 8 lat, usunęło on_startup/on_shutdown/on_event() oraz dekoratory @app.route()/@app.websocket_route() (należy używać lifespan oraz Route/WebSocketRoute); najnowsza wersja to 1.3.1 (2026-06-12). SQLAlchemy 2.0.51 (dziennik zmian, 2026-06-15) zawiera wyłącznie poprawki błędów, bez wpływu na async ani instalację. Zweryfikowano przez PyPI i oficjalne informacje o wydaniach 2026-06-16. 

  25. FastAPI informacje o wydaniach: 0.138.0 (2026-06-20) dodaje app.frontend("/", directory="dist") i router.frontend("/", directory="dist") do serwowania zbudowanego statycznego frontendu (PR #15800; dokumentacja Frontend) — funkcję serwowania statycznego SPA z dist/, a nie wzorzec renderowania po stronie serwera; bez zmian niezgodnych wstecznie. 0.137.2 (2026-06-18) dodaje iter_route_contexts() do zaawansowanego użycia, które wcześniej przechodziło po router.routes (wewnętrznym od 0.137.0); bez zmian niezgodnych wstecznie. Na 2026-06-22 nie było wydania nowszego niż 0.138.0. Starlette 1.3.1, Pydantic 2.13.4, Uvicorn 0.49.0, SQLAlchemy 2.0.51, HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8 — wszystkie bez zmian. Zweryfikowano przez PyPI i oficjalne informacje o wydaniach 2026-06-22. 

  26. FastAPI informacje o wydaniu 0.139.0, 1 lipca 2026 r.: „Support dependencies in app.frontend(), e.g. for automatic cookie authentication for the frontend” (PR #15908). Pozostała część wydania obejmuje tłumaczenia, dokumentację i aktualizacje zależności; bez zmian niezgodnych wstecznie. Weryfikacja w bieżącej sesji 2 lipca 2026 r. (PST): 0.139.0 jest najnowszym wydaniem na stronie wydań GitHub. 

  27. FastAPI informacje o wydaniu 0.140.0, opublikowanym 24 lipca 2026 r. o 21:16 UTC (PyPI upload_time_iso_8601 2026-07-24T21:16:42Z). Jedyny wpis refaktoryzacyjny brzmi: „⚡️ Reduce memory usage in dependencies. PR #16049” (scalony 2026-07-24T21:07:52Z). Regresję wprowadził PR #14262 (scalony 2025-11-03), wydany tego samego dnia w 0.121.0, który dodał functools.cached_property do Dependant.cache_key; do 0.139.2 klasa miała dziesięć definicji @cached_property. W 0.140.0 fastapi/dependencies/models.py deklaruje @dataclass(slots=True) class Dependant, a logikę przeniesiono do funkcji modułowych _get_cache_key(), _get_oauth_scopes(), _uses_scopes() i _is_security_scheme() — źródło zweryfikowano pod tagiem 0.140.0. Bot CodSpeed w scalonym PR raportuje dla benchmarku pamięci test_dependency_graph 17,5 MB (base) → 1,1 MB (head), „improve performance by ×16”; 0.140.0 dodaje też benchmark pamięci w CI (PR #16046), aby zapobiec ponownej regresji. Źródłowy raport to dyskusja #14742, w której 0.120.4 utrzymywało zużycie poniżej około 400 MB, a 0.121.3 kończyło się OOM w produkcji. Uwaga dla autorów narzędzi: Dependant.oauth_scopes, .cache_key, ._uses_scopes i ._is_security_scheme nie istnieją już jako atrybuty, a slots=True uniemożliwia monkey-patching instancji — nieudokumentowany wewnętrzny API, którego ten przewodnik nie używa, w tej samej kategorii co zmiana router.routes w 0.137.0. Wszystkie fakty ponownie zweryfikowano względem PyPI, strony wydań GitHub API i oznaczonego źródła 2026-07-25. 

  28. Wydania Starlette 1.4.0 (2026-08-05), 1.5.0 (2026-08-08) i 1.6.0 (2026-08-08). Wersja 1.5.0 nosi tytuł „This release is all about giving GZipMiddleware some love” i wymienia „Add exclude_content_types parameter to GZipMiddleware”, „Flush GZip output for each streamed chunk”, „Skip compression of partial responses in GZipMiddleware” oraz „Expand default excluded content types in GZipMiddleware”. Krotkę wykluczeń i sygnaturę odczytano bezpośrednio z starlette/middleware/gzip.py: DEFAULT_EXCLUDED_CONTENT_TYPES = application/gzip, application/x-gzip, application/zip, audio/, font/woff, font/woff2, image/avif, image/gif, image/jpeg, image/png, image/webp, text/event-stream, video/ — oraz def __init__(self, app, minimum_size=500, compresslevel=9, thread_minimum_size=128*1024, *, exclude_content_types=DEFAULT_EXCLUDED_CONTENT_TYPES). Wersja 1.6.0 dodaje max_body_size w Starlette/Router/Mount/Route oraz RequestBodyLimitMiddleware. Wszystko pobrano i zweryfikowano 2026-08-16. 

  29. FastAPI 0.141.0 (2026-07-29, 14:47 UTC) dodał app.frontend(check_dir="auto") do lokalnego programowania z fastapi dev (PR #16102). FastAPI 0.141.1 (2026-07-29, 17:17 UTC) naprawił obsługę zadań w tle i nagłówków z zależności w app.frontend() (PR #16105) oraz udokumentował FASTAPI_ENV w przewodniku FastAPI CLI (PR #16104). Oba autorstwa @tiangolo. Najnowszą wersję PyPI 0.141.1 potwierdzono 2026-07-29. 

  30. Wydania FastAPI 0.140.1 do 0.140.7, wszystkie opublikowane 2026-07-27 między 12:07 a 17:34 UTC (PyPI upload_time_iso_8601: 0.140.1 12:07:51Z, 0.140.2 14:15:38Z, 0.140.3 15:30:52Z, 0.140.4 15:46:49Z, 0.140.5 16:02:53Z, 0.140.6 16:31:48Z, 0.140.7 17:34:47Z). Treść każdego wydania zawiera pojedynczy wpis Refactors: 0.140.1 „Update the lru_cache limit for dependencies to account for large apps” (PR #16062); 0.140.2 „Stop retaining flat dependency trees” (PR #16065); 0.140.3 „Avoid repeated dependency flattening in OpenAPI” (PR #16067); 0.140.4 „Skip unused dependency repeat bookkeeping” (PR #16069); 0.140.5 „Avoid flattening dependencies for body fields” (PR #16071); 0.140.6 „Avoid flattening dependencies for request parameters, mainly for OpenAPI” (PR #16073); 0.140.7 „Avoid flattening dependencies for OpenAPI” (PR #16076). Liczba dotycząca cache pochodzi z diffu #16062, który zastępuje trzy dekoratory @lru_cache(maxsize=1024) w fastapi/dependencies/models.py przez @lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE) i aktualizuje tests/test_dependency_models.py, aby sprawdzać cache_info.maxsize == 4096; treść PR stwierdza: „Some users reported a number of dependencies larger than 1024, this should account for larger apps.” Wersja 0.140.2 dodaje również benchmark pamięci (PR #16064), a 0.140.7 dodaje benchmarki zależności OpenAPI (PR #16075), więc pokrycie benchmarkami powstało po większości tej serii. Zweryfikowano względem strony wydań GitHub API, diffów PR i PyPI 2026-07-27; 0.140.7 było najnowszym wydaniem w chwili pisania. 

NORMAL fastapi-htmx.md EOF