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

FastAPI + HTMX: Der Full-Stack ohne Build-Prozess

# Erstellen Sie produktionsreife Web-Apps ohne React oder webpack: FastAPI, HTMX, Alpine.js, Jinja2, reines CSS, Bootstrap-Muster, i18n, Deployment, SEO und Performance.

author: words: 11232 read_time: 57m updated: 2026-08-17 02:16
$ less fastapi-htmx.md

TL;DR: FastAPI + HTMX + Alpine.js + Jinja2 + reines CSS erzeugt produktionsreife Webanwendungen ohne Build-Tools, ohne node_modules/ und mit perfekten Lighthouse-Werten. Dieser Leitfaden behandelt das gesamte System von der Architektur bis zum Deployment und nutzt blakecrosley.com als Produktionsreferenz: 210 Blogbeiträge, interaktive JavaScript-Komponenten, 11 Kernleitfäden, 48 Designstudien sowie Englisch plus 9 übersetzte Locales, ohne einen einzigen Bundler, Compiler oder Transpiler.1

Der moderne Webentwicklungs-Stack setzt voraus, dass Sie React, webpack, TypeScript und eine Build-Pipeline benötigen. Für eine große Kategorie von Anwendungen — contentgetriebene Websites, interne Tools, CRUD-Anwendungen, Portfolio-Websites, Dokumentationsplattformen — ist diese Annahme falsch. Der in diesem Leitfaden beschriebene Stack eliminiert die gesamte Frontend-Build-Toolchain und erzeugt zugleich Websites, die bei Lighthouse 100/100/100/100 erreichen.2

Das ist kein Plädoyer. Es ist eine Messung. Die hier beschriebene Architektur läuft in Produktion, bedient echte Benutzer in zehn Sprachen, und die Zahlen sind überprüfbar.


Wichtigste Erkenntnisse

  • Server-gerendertes HTML eliminiert drei ganze Problemkategorien: Client-State-Management, JSON-Serialisierungsgrenzen und Hydration-Mismatches. HTMX macht Server-Antworten zur finalen Ausgabe — kein clientseitiger Rendering-Schritt.
  • Keine Build-Tools bedeuten keine Build-Fehler. Keine npm install-Konflikte bei Peer-Dependencies, keine TypeScript-Compiler-Fehler in Dateien, die Sie nicht angefasst haben, keine Dependabot-PRs für transitive Abhängigkeiten, die Sie nie importiert haben. Die Deploy-Pipeline ist git push.
  • Alpine.js übernimmt clientseitigen State, den HTMX nicht abbilden kann. Dropdowns, Modals, mobile Navigations-Toggles und sämtlicher UI-State, der ausschließlich im Browser existiert, gehören zu Alpine.js. Die Grenze ist klar: Wenn der State den Server benötigt, verwenden Sie HTMX. Falls nicht, verwenden Sie Alpine.js.
  • Plain CSS mit Custom Properties ersetzt Sass und Tailwind. CSS Custom Properties kaskadieren, vererben sich und reagieren zur Laufzeit auf Media Queries. Präprozessor-Variablen kompilieren zu statischen Werten und verschwinden. Der Browser liest Custom Properties direkt — kein Kompilierungsschritt.
  • Dieser Ansatz hat klare Grenzen. Er ist falsch für große Teams, die Komponenten-Schnittstellen teilen, für SaaS-Produkte mit komplexem clientseitigen State und für Anwendungen, die auf npm-Ökosystem-Bibliotheken angewiesen sind. Der Entscheidungsrahmen in Abschnitt 15 identifiziert die Grenze präzise.
  • blakecrosley.com ist der Beweis. Die Kernmuster in diesem Leitfaden (HTMX, Alpine.js, Jinja2, plain CSS) laufen produktiv auf blakecrosley.com. Die Bootstrap- und SQLAlchemy-Abschnitte behandeln Standardmuster für den Stack, die auf dieser konkreten Seite nicht verwendet werden. Jede Aussage hat einen Dateipfad, einen Konfigurationsblock oder ein Lighthouse-Audit, das Sie selbst unter PageSpeed Insights verifizieren können.2

So nutzen Sie diesen Leitfaden

Dies ist eine umfassende Referenz. Beginnen Sie dort, wo Ihr Erfahrungsniveau passt:

Erfahrung Hier starten Anschließend erkunden
Python-Entwickler, neu bei HTMX The No-Build ThesisArchitecture OverviewHTMX Deep Dive Alpine.js Patterns, Security
React/Vue-Entwickler, der Alternativen evaluiert The No-Build ThesisDecision Framework Architecture Overview, Performance
FastAPI-Entwickler, der Interaktivität hinzufügt HTMX Deep DiveAlpine.js Patterns i18n and Localization, Deployment
Full-Stack-Entwickler, der von Grund auf baut Sequenziell ab Architecture Overview lesen Quick Reference Card zur laufenden Verwendung

Verwenden Sie Strg+F / Cmd+F, um nach bestimmten Mustern oder Attributen zu suchen. Die Quick Reference Card am Ende bietet eine überfliegbare Zusammenfassung.


Die No-Build-These

Die These ist eng und konkret: Für content-getriebene Seiten mit einem einzelnen Entwickler oder kleinen Team lösen Build-Tools Probleme, die Sie nicht haben, und schaffen dabei welche, die Sie schon haben.

Hier sind die echten Metriken von blakecrosley.com:

Metrik blakecrosley.com (No-Build) Typisches Next.js-Projekt3
Abhängigkeiten 17 Python-Pakete 311+ npm-Pakete
Build-Konfigurationsdateien 0 5-8 (next.config, tsconfig, postcss, tailwind, etc.)
Größe von node_modules/ Existiert nicht 187 MB Baseline, 250-400 MB mit Erweiterungen
Installationszeit pip install: 8 Sekunden npm install: 30-90 Sekunden
Build-Schritt Keiner next build: 15-60 Sekunden
Deploy-Pipeline git push → live in ~40 Sekunden Installieren → bauen → deployen: 2-5 Minuten
Lighthouse Performance 100 70-90 ohne explizite Optimierung4

Die 17 Python-Pakete umfassen FastAPI, Jinja2, Pydantic, uvicorn, nh3 und 12 weitere. Keines ist ein Build-Tool. Keines ist ein Compiler. Keines ist ein Bundler.5

Worauf Sie verzichten

Ehrlichkeit verlangt, die echten Kosten zu benennen:

Kein TypeScript. Jede .js-Datei ist Vanilla-JavaScript. Typfehler werden durch Testing und Code-Analyse erkannt, nicht durch einen Compiler. Das funktioniert für einen Solo-Entwickler. Für ein Team von 10 Personen, die Komponenten-Schnittstellen teilen, würde es nicht funktionieren.

Kein Hot Module Replacement. CSS-Änderungen erfordern einen manuellen Browser-Refresh. HTMXs hx-boost macht die Navigation schnell genug, dass volle Refreshes tolerierbar sind, aber bei engen visuellen Iterationszyklen spart HMR Zeit.

Kein Tree Shaking. Jedes Byte JavaScript, das Sie schreiben, wird an den Browser ausgeliefert. Diese Einschränkung erzwingt Disziplin: kleine, fokussierte Dateien statt großer Utility-Module.

Keine npm-Komponentenbibliotheken. Kein Radix, kein shadcn/ui, kein Headless UI. Jedes interaktive Element ist handgebaut oder verwendet die eingebauten Komponenten von Bootstrap 5.

Keine Design-System-Tokens aus npm. Das Design-System lebt in CSS Custom Properties. Es kann nicht als Paket in ein anderes Projekt importiert werden.

Diese Kompromisse sind für eine content-getriebene Seite mit ein bis drei Entwicklern akzeptabel. Für ein SaaS-Produkt mit einem 15-köpfigen Engineering-Team wären sie inakzeptabel. Abschnitt 15 liefert den Entscheidungsrahmen.

Was Sie gewinnen

Keine Build-Fehler. Kein npm install kann an Peer-Dependency-Konflikten scheitern. Kein next build kann an einem TypeScript-Fehler in einer Datei scheitern, die Sie nicht angefasst haben.6

Debugging mit View Source. Das JavaScript, das im Browser läuft, ist das JavaScript, das Sie geschrieben haben. Keine Source Maps erforderlich.

Sofortiger lokaler Start. uvicorn app.main:app --reload startet in unter 2 Sekunden.

Konkreter Request-Wasserfall. Ein erster Besuch lädt: ein HTML-Dokument (~15KB gzipped), eine CSS-Datei (~8KB), HTMX (~16KB, gecacht), Alpine.js (~15KB, gecacht) und das interaktive JS der Seite (~4-8KB). Insgesamt: ungefähr 55-65KB beim ersten Besuch.1

Zukunftssicheres Frontend. Der clientseitige Code verwendet HTML, CSS und JavaScript — Standards, die seit 30 Jahren Abwärtskompatibilität bewahren.7 Keine Webpack-4-→-5-Migration, keine Create-React-App-Abkündigung, keine Next.js-App-Router-Migration.

Stack-Vergleich

Wie sich der No-Build-Stack auf messbaren Dimensionen mit gängigen Alternativen vergleicht:

Dimension FastAPI+HTMX (dieser Leitfaden) Next.js (React) Astro 11ty
An den Browser ausgeliefertes JS 35-40KB (HTMX+Alpine+kleine Seitenskripte) 85-250KB+ (React-Runtime) 0KB Standard, optionale Islands 0KB Standard
Build-Schritt Keiner Erforderlich (webpack/turbopack) Erforderlich (Vite) Erforderlich (custom)
Konfigurationsdateien 0 5-8 (next.config, tsconfig, etc.) 1-3 (astro.config, tsconfig) 1-2 (.eleventy.js)
Deploy-Pipeline git push (40s) Install+Build+Deploy (2-5min) Install+Build+Deploy (1-3min) Install+Build+Deploy (1-2min)
Serverseitige Interaktivität Nativ (HTMX) API-Routes + Client-Fetch Begrenzt (Form Actions) Keine (statische Ausgabe)
Client-State-Management Alpine.js (15KB) React state/context/Redux Framework Islands Manuelles JS
Backend-Sprache Python JavaScript/TypeScript JavaScript/TypeScript JavaScript
i18n-Ansatz Serverseitig (Middleware) next-intl oder ähnliches Paket @astrojs/i18n Manuell
Lighthouse Performance 100 (gemessen) 70-90 typisch4 95-100 typisch 95-100 typisch
Am besten geeignet für Content-Seiten, CRUD, Dashboards Komplexe SPA, große Teams Content-Seiten, Marketing Statische Blogs, Dokumentation

Astro und 11ty sind die nächsten Konkurrenten für Content-Seiten. Beide produzieren exzellente statische Ausgaben, erfordern aber einen Build-Schritt und eine JavaScript-Toolchain. Der FastAPI+HTMX-Stack tauscht Static-Site-Performance gegen serverseitige Interaktivität (Kategoriefilterung, Formularverarbeitung, Echtzeit-Suche), ohne einen Build-Schritt hinzuzufügen. Wenn Ihre Seite rein statisch ist und keine Server-Interaktionen hat, sind Astro oder 11ty möglicherweise die bessere Wahl.


Architekturübersicht

Ablauf einer Anfrage

Jede Anfrage durchläuft einen einzigen Pfad durch vier Schichten:

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 -------------------------------------------------------->|

Vollständige Seitenaufrufe liefern komplette HTML-Dokumente zurück (Basis-Template + Seiten-Template). HTMX-Anfragen liefern HTML-Fragmente (Partials). Der Server entscheidet anhand des Anfragetyps, was gerendert wird. Alpine.js verwaltet clientseitigen Zustand, der den Server nie erreicht.

Komponentenrollen

Komponente Rolle Geltungsbereich
FastAPI Routing, Geschäftslogik, Datenzugriff, Validierung Server
Jinja2 Template-Rendering, Vererbung, Makros Server
HTMX Servergesteuerte Interaktivität (Formulare, Paginierung, Suche) Client ↔ Server
Alpine.js Rein clientseitiger Zustand (Dropdowns, Modals, Toggles) Nur Client
Bootstrap 5 Grid-System, Utility-Klassen, responsives Layout Client (CSS)
Plain CSS Benutzerdefinierte Properties, Komponentenstile, Design-Tokens Client (CSS)
Pydantic Anfrage-/Antwortvalidierung, Einstellungen Server

Projektstruktur

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

Die Struktur folgt einem einzigen Prinzip: Jedes Verzeichnis enthält genau eine Art von Dateien. Routen befinden sich in routes/. Templates in templates/. Statische Assets in static/. Kein Build-Schritt transformiert das eine in das andere.

Vergleich mit der SPA-Architektur

In einem React + Next.js-Projekt sähe die entsprechende Struktur folgendermaßen aus:

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

Die SPA-Architektur erfordert eine Build-Zeit-Koordination zwischen diesen Verzeichnissen. TypeScript kompiliert .tsx zu JavaScript. PostCSS verarbeitet Tailwind-Direktiven zu CSS. Webpack (oder Turbopack) bündelt die Ausgabe in Chunks. Jeder Schritt kann unabhängig fehlschlagen.

Die No-Build-Architektur erfordert keinerlei Koordination. Das Template referenziert eine CSS-Datei. Die CSS-Datei existiert in static/css/. Der Browser lädt sie direkt. Benennen Sie eine Datei um, bricht die Template-Referenz zur Laufzeit — nicht zur Build-Zeit. Damit verlagern sich Fehler von der Kompilierzeit zur Laufzeit, was ein echtes Abwägungsproblem darstellt. Für einen einzelnen Entwickler, der während der Entwicklung uvicorn --reload ausführt, erscheinen Laufzeitfehler sofort im Browser. Für ein großes Team hingegen verhindert die Kompilierzeitprüfung durch TypeScript eine ganze Kategorie von Fehlern, die Laufzeitfehler nicht abfangen können.


FastAPI-Muster

Anwendungseinrichtung

Die Anwendung wird in main.py mit einer expliziten Middleware-Reihenfolge initialisiert:

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)

Drei Designentscheidungen sind hier wichtig. Erstens deaktivieren docs_url=None und openapi_url=None die automatischen API-Dokumentationsendpunkte. Eine öffentlich zugängliche Content-Website muss /docs oder /openapi.json nicht im Internet verfügbar machen.8 Zweitens ist die Middleware-Reihenfolge entscheidend — das Sicherheitslogging wird zuerst ausgeführt (zuletzt hinzugefügt), sodass es jede Anfrage erfasst, auch solche, die vom Rate Limiting abgelehnt werden. Drittens komprimiert GZipMiddleware Antworten über 500 Byte, wodurch sich die Übertragungsgröße von HTML typischerweise um 70–80 % verringert. Seit Starlette 1.5.0 komprimiert sie nicht mehr alles: Eine Standardausschlussliste überspringt bereits komprimierte und binäre Nutzdaten (gzip- und zip-Archive, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/*, WOFF- und WOFF2-Schriften sowie text/event-stream) — genau das ist erwünscht, denn das erneute Komprimieren eines PNG verbraucht CPU und macht es geringfügig größer. Beachten Sie, dass die Liste image/* bewusst ausschließt, sodass image/svg+xml weiterhin komprimiert wird. Sie können sie mit dem Keyword-only-Parameter exclude_content_types überschreiben.28

Routing

Routen lassen sich in zwei Kategorien unterteilen: Seitenrouten geben vollständige HTML-Dokumente zurück, API-Routen geben dagegen JSON- oder HTML-Fragmente zurück.

# 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,
    })

Diese Unterscheidung ist für HTMX wichtig. Vollständige Seitenrouten geben Dokumente zurück, die base.html erweitern. API-Routen geben HTML-Fragmente zurück, die HTMX in bestehende DOM-Elemente einfügt. Beide werden von derselben Jinja2-Template-Engine gerendert — es gibt keine separate API-Schicht.

Dependency Injection

Das Depends()-System von FastAPI sorgt für eine saubere Trennung zwischen Route-Handlern und gemeinsam genutzter 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,
    })

Abhängigkeiten lassen sich zusammensetzen. Eine get_db-Abhängigkeit kann von get_current_locale abhängen, die wiederum von der Anfrage abhängt. FastAPI löst diese Kette automatisch auf.

Pydantic-Einstellungen

Die Konfiguration verwendet Pydantics BaseSettings mit Vorrang für Umgebungsvariablen:

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()

Umgebungsvariablen überschreiben Werte aus der .env-Datei. In der Produktion (Railway) werden Secrets als Umgebungsvariablen gesetzt. Lokal liefert eine .env-Datei Standardwerte. Die Klasse Settings validiert Typen beim Start — ein fehlendes Pflichtfeld führt sofort zu einem Fehler statt erst zur Laufzeit.

Async-Muster

FastAPI-Routen sind standardmäßig async. Bei I/O-gebundenen Vorgängen (Datenbankabfragen, HTTP-Anfragen, Dateizugriffen) verhindert async, dass die Event Loop blockiert wird:

@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 ist jetzt der einzige Pfad für Start und Herunterfahren. Starlette erreichte im März 2026 mit 1.0 seine erste stabile Veröffentlichung (am 8. August war 1.6.0 aktuell) und entfernte die seit Langem veralteten Hooks on_event, on_startup und on_shutdownlifespan (oben) ist der einzige Mechanismus, und @app.route() / @app.websocket_route() wichen Route / WebSocketRoute in der Liste routes. FastAPI 0.137.0 (14. Juni 2026) gestaltet seine eigenen Router-Interna um: router.routes ist nicht mehr eine flache Liste von APIRoute-Objekten, sondern ein Baum aus Zwischenknoten. Behandeln Sie sie daher als internes Detail, statt darüber zu iterieren. Positiv ist, dass Routen, die einem Router nach include_router() hinzugefügt werden, nun live berücksichtigt werden und ein Unterrouter eingebunden werden kann, bevor seine Routen definiert sind. FastAPI selbst beschränkt Starlette nicht auf die 1.x-Linie: Seit 0.136.3 lautet die Laufzeitanforderung lediglich starlette>=0.46.0, und sie ist bis einschließlich 0.140.7 unverändert — keine Obergrenze, und Starlette 0.4x erfüllt sie weiterhin. Die Versionsnummern 1.x in den Release Notes zu 0.137.0 sind dependabot-Updates der eigenen Test-Lockdatei des Repositorys, keine Laufzeitbeschränkung für Ihre Anwendung.24 Nichts davon verändert die Muster in diesem Leitfaden — er verwendet durchgehend lifespan und Standardroutendeklarationen — aber wenn Sie Tools pflegen, die router.routes durchlaufen, oder noch ältere @app.on_event-Handler einsetzen, sind 0.137.0 / Starlette 1.0 inkompatible Änderungen. FastAPI 0.137.2 (18. Juni 2026) folgt mit iter_route_contexts(), dem nun unterstützten Weg zum Auflisten von Routen, da router.routes intern ist. FastAPI 0.138.0 (20. Juni 2026) ergänzt anschließend app.frontend("/", directory="dist") / router.frontend(...) zum Bereitstellen eines gebauten statischen Frontends — nützlich, wenn Sie einen separaten SPA-Build ausliefern, jedoch unabhängig vom Build-freien, servergerenderten Ansatz dieses Leitfadens (es mountet einen dist/-Ordner, statt HTML auf dem Server zu rendern).25 FastAPI 0.139.0 (1. Juli 2026) erweitert dies um Dependency Support in app.frontend() — beispielsweise automatische Cookie-Authentifizierung für das bereitgestellte Frontend — und bringt damit dieselbe Depends()-Mechanik, die Sie in API-Routen verwenden, zum statischen Frontend-Mount.26 FastAPI 0.141.0 (29. Juli 2026) ergänzt app.frontend(check_dir="auto"), wodurch fastapi dev nicht mehr fehlschlägt, wenn das Build-Verzeichnis noch nicht existiert — der übliche Fall, dass Sie den Server starten, bevor Sie den Frontend-Build ausführen. FastAPI 0.141.1, noch am selben Tag veröffentlicht, behebt, dass Abhängigkeiten in app.frontend() Hintergrundaufgaben und Response-Header stillschweigend verwarfen: Eine Abhängigkeit, die ein Cookie setzte oder eine BackgroundTask plante, verlor diese Arbeit beim Frontend-Mount, während sie auf API-Routen normal funktionierte. Wenn Sie den Dependency Support aus 0.139.0 übernommen haben, sorgt erst 0.141.1 dafür, dass er sich wie der Rest der Anwendung verhält.29

FastAPI 0.140.0 beendet eine Speicherregression, die seit November 2025 in jeder Veröffentlichung enthalten war — führen Sie ein Upgrade durch. Die Veröffentlichung vom 24. Juli 2026 besteht aus einem einzigen Refactoring mit unverhältnismäßig großer Wirkung. Dependant, das interne Objekt, das FastAPI für jeden Knoten jedes Abhängigkeitsgraphen einer Route erstellt, hatte ab 0.121.0 (3. November 2025) functools.cached_property-Attribute angesammelt — bis 0.139.2 waren es zehn. Eine gecachte Property benötigt ein instanzspezifisches __dict__, um ihr Ergebnis darin zu speichern, sodass sich die Kosten über jeden Knoten jedes Graphen in der Anwendung vervielfachten. PR #16049 verschiebt diese Logik aus der Klasse in Hilfsfunktionen auf Modulebene (_get_cache_key(), _get_oauth_scopes(), _uses_scopes()) und deklariert Dependant als @dataclass(slots=True), sodass es ein reiner Datencontainer bleibt. Der eigene CodSpeed-Lauf von FastAPI für den gemergten PR beziffert den Speicherbenchmark test_dependency_graph mit 17,5 MB → 1,1 MB, einer Reduzierung um das 16-Fache; der Bericht, der die Arbeit ausgelöst hatte, beschrieb einen Produktionsservice, der unter 0.120.4 bei ungefähr 400 MB blieb und unter 0.121.3 einen OOM erreichte. Jede seitdem in diesem Leitfaden empfohlene Version — 0.137.x, 0.138.0, 0.139.2 — enthielt die Regression. Wenn Ihre Anwendung einen tiefen oder breiten Abhängigkeitsbaum hat (verschachtelte Depends(), Sicherheitsschemas, viele eingebundene Router), ist 0.140.0 ein kostenloser Speichergewinn, der keine Änderung am Anwendungscode erfordert.27

0.140.0 war der Auftakt, nicht die vollständige Lösung — pinnen Sie 0.140.7 oder neuer. Drei Tage nach dieser Veröffentlichung, am 27. Juli 2026, veröffentlichte FastAPI sieben weitere Versionen innerhalb von fünfeinhalb Stunden: 0.140.1 bis 0.140.7, jede davon ein Refactoring derselben Abhängigkeitsmechanik. Die Arbeit teilt sich in zwei Bereiche. Erstens der flache Abhängigkeitsbaum: FastAPI erstellte und behielt früher eine abgeflachte Kopie des Abhängigkeitsgraphen jeder Route. 0.140.2 speichert sie nicht mehr, während 0.140.3, 0.140.5, 0.140.6 und 0.140.7 die verbliebenen Stellen entfernten, die eine solche Kopie neu erstellten — OpenAPI-Generierung, Body-Felder, Anfrageparameter und nochmals OpenAPI. 0.140.4 entfernt Buchhaltung, die wiederholte Abhängigkeiten verfolgte, obwohl sie niemand auslas. Zweitens, und die einzige Änderung mit einem sichtbaren Schwellenwert: 0.140.1 erhöht den lru_cache der Hilfsfunktionen zur Callable-Klassifizierung in fastapi/dependencies/models.py von 1.024 auf 4.096 Einträge, hinter einer benannten Konstante _CALLABLE_CLASSIFICATION_CACHE_SIZE, weil Benutzer Anwendungen mit mehr als 1.024 unterschiedlichen Abhängigkeiten meldeten, die den Cache ständig verdrängten. Nichts davon ändert eine API, die Sie aufrufen, daher ist das Upgrade eine Versionsanhebung. Zwei Einschränkungen sollten klar genannt werden: Die schnelle Abfolge bedeutet, dass sich die Linie noch bewegt. Lesen Sie daher die Release Notes, statt anzunehmen, 0.140.7 sei das Ende; und FastAPI fügte die OpenAPI-Dependency-Benchmarks, die diese Arbeit messen, im selben Zeitraum hinzu (PR #16075), sodass die veröffentlichten Zahlen die letzten paar Releases abdecken, nicht den gesamten Bogen über sieben Versionen.30

CPU-gebundene Vorgänge (Markdown-Rendering, CSS-Extraktion) können synchrone Funktionen verwenden. FastAPI führt sie automatisch in einem Thread Pool aus, wenn der Route-Handler nicht als async deklariert ist:

# 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(...)

Die Regel: Wenn die Funktion auf I/O wartet, machen Sie sie async. Wenn sie CPU-Arbeit ausführt, lassen Sie sie synchron. Mischen Sie nicht await mit blockierenden Aufrufen in derselben Funktion.9

Jinja2-Templates

Template-Vererbung

Das Vererbungssystem von Jinja2 ersetzt Reacts Komponentenkomposition durch ein einfacheres Modell. Ein einziges Basis-Template definiert das Seitengerüst. Kind-Templates füllen benannte Blöcke:

<!-- 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 %}

Die {% extends %}-Direktive stellt eine Eltern-Kind-Beziehung her. Das Kind-Template definiert nur die Blöcke, die es überschreiben muss. Alles andere — der <head>, der Header, der Footer, die Script-Tags — stammt aus dem Basis-Template. Komposition erfolgt hier durch Subtraktion statt durch Konstruktion.

Das asset()-Global

Statische Assets verwenden Content-Hash-Versionierung für Cache-Busting:

# 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}"

Im Template rendert {{ asset('css/styles.css') }} zu /static/css/styles.css?v=a3f8b2c1d0. Der Hash ändert sich bei Dateiänderungen und invalidiert damit den CDN-Cache. Webpacks [contenthash]-Dateinamenstrategie wird so durch 30 Zeilen Python ersetzt, die beim Start berechnet werden.

Include für wiederverwendbare Partials

Komponenten, die sich über mehrere Seiten hinweg wiederholen, nutzen {% 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>

Der Unterstrich-Präfix (_language_switcher.html) ist eine Konvention für Partials — Template-Fragmente, die nicht eigenständig gerendert werden sollen. Diese Komponente nutzt sowohl Alpine.js (für den Dropdown-Toggle) als auch Jinja2 (für die Sprachliste). Die Grenze ist klar gezogen: Alpine.js verwaltet den Öffnen/Schließen-Zustand, Jinja2 die Daten.

Macros für wiederverwendbare Komponenten

Macros sind die Funktionen von Jinja2 — wiederverwendbare Template-Blöcke mit Parametern:

<!-- 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 %}

Macros werden in Seiten-Templates importiert und verwendet:

{% 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>

Macros ersetzen React-Komponenten für Präsentationsmuster. Sie akzeptieren Parameter, unterstützen Standardwerte und lassen sich mit anderen Macros kombinieren. Der entscheidende Unterschied: Macros werden einmalig auf dem Server gerendert und erzeugen statisches HTML. React-Komponenten hingegen werden auf dem Client gerendert und verwalten einen Zustand. Für die Darstellung von Inhalten sind Macros das richtige Werkzeug.

Template-Kontext und Globals

Jinja2-Globals sind Funktionen, die in jedem Template ohne explizite Übergabe verfügbar sind:

# 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

Das asset()-Global erzeugt versionierte URLs. Das csrf_token()-Global generiert frische CSRF-Token. Das analytics_script()-Global fügt das Tracking-Snippet ein. Diese Funktionen sind in jedem Template aufrufbar, ohne dass der Route-Handler sie explizit übergeben muss.

Für i18n ist die Einrichtung aufwendiger — Übersetzungsfunktionen benötigen Zugriff auf die Locale des aktuellen Requests:

# 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

Jede Funktion liest die Locale aus der Kontextvariable des Requests, die von der Locale-Middleware gesetzt wird. Im Template liefert der Aufruf {{ _('ui.nav.about') }} den übersetzten String für die Locale des aktuellen Requests — ganz ohne expliziten Locale-Parameter.

Bedingte Blöcke

Das Blocksystem von Jinja2 unterstützt bedingte Überschreibungen:

<!-- 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 %}

Blogbeiträge deklarieren ihre Abhängigkeiten im YAML-Frontmatter (scripts: ["/static/js/boids.js"]). Das Template bindet diese bedingt ein. Seiten ohne zusätzliche Skripte oder Styles liefern auch keine aus — kein toter Code, keine ungenutzten Importe.

Benutzerdefinierte Filter

Jinja2-Filter transformieren Daten während des Renderings. Der sanitize-Filter verhindert XSS in nutzergenerierten Inhalten:

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

Im Template: {{ user_content | sanitize }}. Die nh3-Bibliothek ist ein Rust-basierter HTML-Sanitizer — schnell und sicher. Sie entfernt sämtliche Tags und Attribute, die nicht auf der Erlaubnisliste stehen, und verhindert so Stored XSS selbst dann, wenn der Inhalt aus einer nicht vertrauenswürdigen Quelle stammt.10


HTMX im Detail

HTMX macht jedes HTML-Element fähig, HTTP-Anfragen zu senden und die Antwort in das DOM einzufügen. Die zentrale Erkenntnis ist architektonischer Natur: Serverseitig gerendertes HTML ist die API. Der Server liefert die endgültige Darstellung. Kein clientseitiges Rendering, keine JSON-Serialisierung, keine Hydration.

Kernattribute

Attribut Zweck Beispiel
hx-get GET-Anfrage senden hx-get="/search?q=term"
hx-post POST-Anfrage senden hx-post="/contact"
hx-target Ziel für die Antwort hx-target="#results"
hx-swap Art der Einfügung hx-swap="innerHTML" (Standard), outerHTML, beforeend
hx-trigger Auslöser für die Anfrage hx-trigger="click", keyup changed delay:300ms, load
hx-indicator Element, das während der Anfrage angezeigt wird hx-indicator="#spinner"
hx-push-url Browser-URL aktualisieren hx-push-url="true"
hx-replace-url URL ohne Verlaufseintrag ersetzen hx-replace-url="true"

Muster 1: Interaktives Quiz (mehrstufiger Serverstatus)

blakecrosley.com enthält ein interaktives Quiz, das Benutzer durch die Werkzeugauswahl führt. Der gesamte Quiz-Zustand liegt auf dem Server — kein clientseitiges State Management:

<!-- _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>

Jeder Klick auf eine Schaltfläche sendet die gesammelten Antworten als Query-Parameter. Der Server berechnet die nächste Frage oder das Endergebnis anhand der Antworthistorie. Der Zustand akkumuliert sich in der URL — keine Cookies, keine Sessions, kein clientseitiges JavaScript. Das Quiz schreitet durch outerHTML-Swaps voran: Jede Antwort ersetzt das gesamte Quiz-Schritt-Element.

Muster 2: Paginierte Blog-Liste

Die Schreibseite nutzt HTMX für nahtlose Paginierung mit URL-Aktualisierung:

<!-- 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>

Vier Attribute arbeiten zusammen:

  1. hx-get sendet die Anfrage an dieselbe URL wie das href (Progressive Enhancement — funktioniert auch ohne JavaScript)
  2. hx-target platziert die Antwort im #writing-content-Container
  3. hx-replace-url="true" aktualisiert die Browser-URL, ohne einen Verlaufseintrag hinzuzufügen
  4. hx-indicator zeigt einen Lade-Spinner während der Anfrage an

Der Server erkennt HTMX-Anfragen über den HX-Request-Header und liefert nur das Beitragslisten-Fragment statt der vollständigen Seite zurück. Deshalb fügt die Security-Headers-Middleware Vary: HX-Request hinzu — damit CDN-Caches die vollständige Seite und das Fragment getrennt speichern.11

Muster 3: Suche mit 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>

Das hx-trigger-Attribut kombiniert drei Modifikatoren:

  • keyup löst beim Loslassen einer Taste aus
  • changed löst nur aus, wenn sich der Wert tatsächlich geändert hat (verhindert doppelte Anfragen durch Modifikatortasten)
  • delay:300ms entprellt — wartet 300 ms nach dem letzten Keyup, bevor ausgelöst wird

Der Server liefert ein gerendertes HTML-Fragment zurück:

@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,
    })

Kein clientseitiger Zustand. Keine Debounce-Bibliothek. Kein useEffect. Das Template rendert die Ergebnisse, HTMX fügt sie ein, und der Server bleibt die einzige Quelle der Wahrheit.

Muster 4: Out-of-Band (OOB) Swaps

Manchmal muss eine einzelne Serveraktion mehrere DOM-Elemente aktualisieren. Der Out-of-Band-Swap-Mechanismus von HTMX bewältigt dies ohne clientseitige Orchestrierung:

<!-- 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>

Das Attribut hx-swap-oob="true" weist HTMX an, das Element anhand seiner id überall im DOM zu finden und zu ersetzen — unabhängig vom hx-target. Dies ersetzt Reacts „State nach oben heben”-Muster: Der Server berechnet den gesamten abgeleiteten Zustand und sendet das fertige HTML für jedes Element in einer einzigen Antwort.

Ein Kontaktformular veranschaulicht dies gut: Das Absenden des Formulars könnte den Formularkörper durch eine Erfolgsmeldung ersetzen und gleichzeitig ein Benachrichtigungs-Badge über einen OOB-Swap aktualisieren:

HTMX kann Standard-Navigationslinks „boosten”, sodass sie AJAX statt vollständiger Seitenladevorgänge verwenden:

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

Mit hx-boost="true" ruft ein Klick auf einen Link die Seite per AJAX ab, tauscht den <body>-Inhalt aus und aktualisiert die URL — ohne vollständigen Seitenneulade. Die Browserhistorie funktioniert normal (Vor-/Zurück-Tasten). Falls JavaScript fehlschlägt, funktionieren die Links als reguläre Navigation.

Der Vorteil liegt in der wahrgenommenen Performance: Geboostete Navigation fühlt sich sofort an, da der Browser weder CSS neu parsen, noch Skripte neu auswerten oder das Layout neu rendern muss. Nur der <body>-Inhalt ändert sich. Boosted Links eignen sich besonders für Hauptnavigationselemente und lassen Seitenwechsel wie eine Single-Page Application wirken — ohne die SPA-Architektur.

Muster 6: HTMX Request-Header

HTMX sendet benutzerdefinierte Header mit jeder Anfrage:

Header Wert Anwendungsfall
HX-Request true HTMX-Anfragen serverseitig erkennen
HX-Target Element-ID Zielelement der Antwort identifizieren
HX-Trigger Element-ID Auslösendes Element identifizieren
HX-Current-URL Vollständige URL Aktuelle Seite des Benutzers ermitteln

Der Server kann HX-Request nutzen, um unterschiedliche Antworten zurückzugeben:

@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)

Dieses Dual-Response-Muster ist zentral für die Architektur. Ein vollständiger Seitenladevorgang liefert das komplette Dokument (Basis-Template + Seiteninhalt). Eine HTMX-Navigation liefert nur den geänderten Inhalt. Der Server entscheidet — nicht der Client.

Muster 7: Progressive Enhancement

Jeder HTMX-Link auf blakecrosley.com enthält ein Standard-href-Attribut:

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

Falls JavaScript nicht geladen werden kann, funktioniert das href als normaler Link. Wird HTMX geladen, fängt es den Klick ab und führt einen AJAX-Swap durch. Das ist Progressive Enhancement: Die Seite funktioniert ohne JavaScript, und HTMX verbessert das Erlebnis, wenn verfügbar.

Muster 8: Ladezustände

<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 fügt dem auslösenden Element während Anfragen die Klasse htmx-request hinzu. Das hx-indicator-Attribut verweist auf ein Element, das während der Anfrage sichtbar wird. Gestylt wird es mit CSS:

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

Kein Ladezustand-Management. Kein useState(false). Kein setLoading(true). CSS steuert die Sichtbarkeit, HTMX übernimmt den Klassen-Toggle.


Alpine.js-Muster

Alpine.js füllt die Lücke, die HTMX offenlässt: clientseitiger Zustand, der niemals den Server berühren muss. Wenn ein Benutzer auf ein Dropdown klickt und es sich öffnet, existiert dieser Zustand ausschließlich im Browser. Alpine.js verwaltet ihn mit HTML-Attributen.

Die Grenzregel

Die Grenze zwischen HTMX und Alpine.js ist klar definiert:

Zustandstyp Werkzeug Beispiel
Benötigt Serverdaten HTMX Suchergebnisse, Formularvalidierung, Paginierung
Existiert nur im Browser Alpine.js Dropdown öffnen/schließen, mobiles Menü umschalten, Modal-Sichtbarkeit
Kombiniert beides Beide Sprachumschalter (Alpine.js-Toggle, HTMX-artige Navigation)

Mobile Navigation

Das Basis-Template umschließt den gesamten Header mit einer Alpine.js-Komponente:

<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>

Wichtige Alpine.js-Muster:

  • x-data deklariert den Komponentenbereich und den Anfangszustand
  • x-show steuert die Sichtbarkeit basierend auf dem Zustand (nutzt CSS display: none)
  • x-cloak verbirgt das Element, bis Alpine.js initialisiert ist (verhindert ein Aufblitzen ungestylter Inhalte)
  • @click bindet Klick-Handler mit Ausdrücken
  • :aria-expanded (Kurzform für x-bind:aria-expanded) setzt Attribute dynamisch
  • @keydown.escape.window lauscht global auf die Escape-Taste, um Panels zu schließen

Der Sprachumschalter verwendet Alpine.js für den Toggle-Zustand mit @click.away zum Schließen bei Klick außerhalb:

<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>

Der @click.away-Modifier schließt das Dropdown bei Klick außerhalb. Alpine.js erledigt dies mit einem einzigen Attribut — kein Event-Listener-Registrierung, kein Aufräumen, kein Ref-Management.

Wann Alpine.js und wann Vanilla JavaScript

Alpine.js eignet sich, wenn:

  • Der Zustand auf ein einzelnes DOM-Element beschränkt ist (Dropdown, Modal, Toggle)
  • Interaktionen binär oder einfach sind (öffnen/schließen, ein-/ausblenden, umschalten)
  • Mehrere Elemente auf dieselbe Zustandsänderung reagieren müssen
  • Barrierefreiheits-Attribute mit der Sichtbarkeit synchron bleiben müssen

Vanilla JavaScript eignet sich, wenn:

  • Die Interaktion komplexe Berechnungen erfordert (Visualisierungen, Simulationen)
  • Die Komponente eine eigene Render-Schleife hat (Canvas, Animation)
  • Performance entscheidend ist (Alpine.js erzeugt Overhead pro x-data-Komponente)
  • Die Logik 20–30 Zeilen Alpine.js-Ausdrücke übersteigt

blakecrosley.com verwendet Alpine.js für Navigation, Sprachumschaltung und Inhalts-Toggles. Die 20 interaktiven Blog-Komponenten (Boids-Simulation, Hamming-Code-Visualisierer usw.) nutzen Vanilla JavaScript, da sie Canvas-Rendering und komplexe Zustandsautomaten erfordern.


End-to-End-Beispiel: Kategoriefilterung auf /writing

Dieser Abschnitt verfolgt ein reales Feature aus der Produktions-Codebasis durch jede Schicht: Route, Template, HTMX-Interaktion, Sicherheit, Caching und gerendertes Ergebnis. Das Feature: Kategorie-Tabs auf der Writing-Seite, die Blogbeiträge ohne vollständiges Neuladen der Seite filtern.

Die Route (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,
    )

Die HX-Request-Header-Prüfung ist das zentrale Muster: dieselbe Route, dieselben Daten, unterschiedliches Template. HTMX erhält ein Fragment. Browser erhalten die vollständige Seite.

Die Kategorie-Tabs (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>

Jeder Tab hat sowohl href (funktioniert ohne JavaScript) als auch hx-get (tauscht nur die Beitragsliste aus). hx-push-url aktualisiert die Browser-URL, sodass die gefilterte Ansicht teilbar und als Lesezeichen speicherbar ist.

Das Partial (pages/writing/_post_list.html)

Das Partial rendert identisch, ob es beim Seitenladen eingebunden oder von HTMX ausgetauscht wird:

{% 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 %}

Kein spezielles HTMX-Markup im Partial. Keine clientseitige Rendering-Logik. Dasselbe HTML funktioniert für das initiale Laden der Seite und jeden nachfolgenden Filter.

Sicherheit

Kategoriewerte werden vor dem Filtern gegen CATEGORY_MAP (ein serverseitiges Dictionary) validiert. Ungültige Kategorien werden ignoriert, nicht zurückgegeben. Keine Benutzereingaben werden in SQL oder HTML interpoliert. Der CSP-Header blockiert Inline-Skripte.

Caching

Kategorie-Antworten sind dynamisch (kein CDN-Cache). Statische Assets (CSS, HTMX, Alpine.js) sind jedoch inhaltlich gehasht und nach dem ersten Laden unbegrenzt gecacht. Nachfolgende Kategoriewechsel übertragen nur das HTML-Partial (~3–5 KB) — kein CSS, kein JS, keine Bilder werden erneut geladen.

Was dieses Beispiel zeigt

Ein Feature, echter Produktionscode, null Build-Tools. Der Server filtert und rendert HTML. HTMX tauscht die Beitragsliste aus. Alpine.js ist nicht beteiligt (kein clientseitiger Zustand nötig). Die URL wird für Teilbarkeit aktualisiert. Progressive Enhancement: Die Tabs funktionieren als gewöhnliche Links ohne JavaScript. Eigener JavaScript-Code für dieses Feature insgesamt: null Zeilen.


Optionale Erweiterungen

Die folgenden Abschnitte behandeln Muster, die den Kern-Stack ergänzen, aber auf blakecrosley.com nicht eingesetzt werden. Sie sind enthalten, weil sie die häufigsten Ergänzungen darstellen, die Teams bei der Einführung dieser Architektur vornehmen.


Bootstrap 5 ohne Sass

Hinweis: blakecrosley.com verwendet reines CSS mit Custom Properties — kein Bootstrap. Dieser Abschnitt behandelt Bootstrap 5 als Option für Teams, die ein Utility-Framework ohne Build-Schritt einsetzen möchten. Das kompilierte CSS von Bootstrap lässt sich über ein CDN laden oder in Ihr Stylesheet einbinden. Die folgenden Muster sind generisch und funktionieren zusammen mit dem in vorherigen Abschnitten beschriebenen HTMX- und Alpine.js-Ansatz.

Bootstrap 5 hat die jQuery-Abhängigkeit entfernt und unterstützt die eigenständige Verwendung von CSS. Weder Sass noch PostCSS oder andere Build-Tools sind erforderlich, um Bootstraps Grid-System und Utility-Klassen zu nutzen.

CDN-freies Self-Hosting

blakecrosley.com hostet alle Vendor-Bibliotheken selbst:

<!-- 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>

Self-Hosting eliminiert externe Abhängigkeiten, verhindert, dass CDN-Ausfälle die Seite beeinträchtigen, und ermöglicht unveränderliches Caching mit Content-Hash-URLs. Laden Sie das kompilierte CSS von Bootstrap herunter (nicht die Sass-Quelldateien) und legen Sie es unter static/css/vendor/ ab.

Grid-System

Bootstraps Grid funktioniert mit einfachen HTML-Klassen:

<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>

Keine Sass-Mixins. Kein @include make-col(). Das kompilierte CSS enthält bereits die responsiven Grid-Klassen. Für benutzerdefinierte Breakpoints über Bootstraps Standardwerte hinaus schreiben Sie einfache CSS-Media-Queries.

Einfache CSS-Überschreibungen

Überschreiben Sie Bootstraps Standardwerte mit CSS Custom Properties und Standard-Selektoren:

/* 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;
}

CSS Custom Properties kaskadieren durch das DOM, werden von übergeordneten Elementen vererbt und reagieren zur Laufzeit auf Media Queries. Sass-Variablen hingegen werden zu statischen Werten kompiliert und verschwinden. Dieser Unterschied ist entscheidend für Theming: Eine einzige Änderung einer Custom Property kann jeden abgeleiteten Wert aktualisieren — ganz ohne Neukompilierung.12

Utility-Klassen vs. Komponenten-CSS

Verwenden Sie Bootstrap-Utility-Klassen für einmalige Abstände und Layouts. Für wiederkehrende Muster setzen Sie auf Komponenten-CSS:

<!-- 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;
}

Das Prinzip: Bootstrap-Utilities für Layout-Mechaniken (Margin, Padding, Flexbox). Benutzerdefiniertes CSS für die visuelle Identität (Farben, Typografie, Animationen). Mischen Sie niemals Utility-Klassen und Komponenten-Styling für denselben Zweck.


Internationalisierung und Lokalisierung

blakecrosley.com stellt Inhalte in 10 Sprachen bereit: Englisch, Japanisch, Koreanisch, vereinfachtes Chinesisch, traditionelles Chinesisch, Deutsch, Französisch, Spanisch, Polnisch und Portugiesisch (Brasilianisch).

URL-basiertes Locale-Routing

Das Locale befindet sich im URL-Pfad: /about (Englisch), /ja/about (Japanisch), /zh-Hans/about (vereinfachtes Chinesisch). Englisch ist die Standardsprache und hat kein Präfix.

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

Die Locale-Middleware extrahiert das Locale aus dem URL-Pfad:

# 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

Die Middleware entfernt das Locale-Präfix vor dem Route-Matching. Dadurch benötigen Route-Handler keine locale-spezifischen Pfade — /about verarbeitet sowohl Englisch (/about) als auch Japanisch (/ja/about), da die Middleware den Pfad normalisiert.

Übersetzungsfunktionen in Templates

Jinja2-Globals stellen Übersetzungsfunktionen bereit:

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

Die Funktion _() sucht einen Übersetzungsschlüssel im Speicher-Cache. Der Filter | default() liefert den englischen Fallback, falls die Übersetzung fehlt. Die Funktion locale_prefix() gibt das URL-Präfix für das aktuelle Locale zurück ("" für Englisch, "/ja" für Japanisch).

Hreflang-Tags

Jede Seite enthält Hreflang-Tags für alle unterstützten Locales:

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

Das erzeugt:

<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">

Suchmaschinen verwenden Hreflang, um in den Suchergebnissen die korrekte Sprachversion auszuliefern. Der x-default-Eintrag verweist auf die englische Version als Fallback.13

Übersetzungsspeicherung und Speicher-Cache

Übersetzungen werden in Cloudflare D1 (SQLite an der Edge) gespeichert und über den lifespan-Handler in einen In-Memory-Cache geladen:

@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)

Der Speicher-Cache vermeidet Datenbankabfragen bei jedem Seitenaufruf. Übersetzungsaktualisierungen erfordern eine Cache-Aktualisierung (ausgelöst über einen Admin-Endpunkt oder ein Deployment). Diese Architektur opfert Aktualität zugunsten der Performance — Übersetzungen ändern sich selten, Seitenaufrufe hingegen erfolgen bei jeder Anfrage.

Gesundheitsüberwachung

blakecrosley.com enthält einen i18n-Health-Check-Endpunkt, der die Übersetzungsabdeckung pro Locale überwacht:

@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

Der Schwellenwert von 99,5 % Abdeckung erkennt fehlende Übersetzungen, bevor Benutzer auf unübersetzte Zeichenketten stoßen. Der Health-Endpunkt ist in das Monitoring von Railway integriert und warnt bei sinkender Abdeckung — beispielsweise nach dem Hinzufügen neuer UI-Zeichenketten, die noch nicht übersetzt wurden.

Locale-abhängiges Content-Rendering

Blogbeiträge und Guides unterstützen locale-spezifische Übersetzungen von Metadaten und Inhalten:

# 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 }}

Das Muster ist konsistent: zuerst den übersetzten Inhalt verwenden, bei Fehlen auf Englisch zurückfallen. So ist eine teilweise Übersetzung möglich — ein japanischer Benutzer sieht übersetzte Titel und Beschreibungen, auch wenn der vollständige Artikeltext auf Englisch bleibt. Der Jinja2-Filter | default() kodiert dieses Muster in einer einzigen Pipe:

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

Locale-Datenübersetzung

Statische Inhalte wie Projektbeschreibungen und Navigationsbezeichnungen werden über Hilfsfunktionen übersetzt, die dieselbe Datenstruktur beibehalten und locale-spezifische Zeichenketten einfügen:

# 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

Dieser Ansatz hält die Übersetzungsschicht von der Datenschicht getrennt. Routen übergeben unabhängig vom Locale dieselbe projects-Liste. Die Übersetzungsfunktionen umhüllen die Daten transparent.

Sitemap mit Hreflang-Alternativen

Die dynamische Sitemap enthält alle Seiten in allen Locales mit Querverweisen:

@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}"/>'
                )

Das ergibt 10 URL-Einträge pro Seite (einen pro Locale), jeweils mit 11 alternativen Links (10 Locales + x-default). Bei einer Website mit 50 Seiten enthält die Sitemap 500 URL-Einträge mit 5.500 Hreflang-Links. Die Sitemap wird dynamisch generiert und für eine Stunde zwischengespeichert.


Datenbankmuster

Hinweis: blakecrosley.com verwendet Cloudflare D1 (serverless SQLite) über HTTP für alle persistenten Daten, nicht SQLAlchemy. Dieser Abschnitt behandelt das standardmäßige async-Muster von SQLAlchemy für FastAPI-Projekte, die eine relationale Datenbank benötigen — das häufigste Produktionssetup für diesen Stack.

SQLAlchemy 2.0 Async

Für Anwendungen, die eine relationale Datenbank benötigen, lässt sich die async-Unterstützung von SQLAlchemy 2.0 sauber in FastAPI integrieren:

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

Installationshinweis (SQLAlchemy 2.0.50+): Seit 2.0.50 wird die greenlet-Abhängigkeit des async-Stacks nicht mehr standardmäßig installiert. Verwenden Sie das asyncio-Extra, damit sie mitinstalliert wird; andernfalls schlägt das erste await gegen die Engine mit einem Missing-greenlet-Fehler fehl:23

pip install "sqlalchemy[asyncio]" aiosqlite

SQLAlchemy 2.0.50 erfordert außerdem Python 3.10+ (3.7–3.9 wurden eingestellt) und fügt Wheels für Free-Threaded (3.13t) hinzu.23

Dependency Injection für Datenbanksitzungen

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
    })

Die get_db-Dependency verwaltet den Sitzungslebenszyklus: Sie öffnet eine Sitzung, übergibt sie per Yield an den Route Handler, committet bei Erfolg und führt bei einer Exception ein Rollback aus. Jede Datenbankoperation verwendet parametrisierte Abfragen — niemals String-Interpolation.

Pydantic-Integration

Pydantic-Modelle validieren Eingaben an der API-Grenze und serialisieren Ausgaben für Templates:

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 validiert Typen, Formate (E-Mail, URL) und Constraints (min./max. Länge), bevor der Route Handler ausgeführt wird. Ungültige Eingaben geben automatisch eine 422-Antwort zurück. Das ersetzt clientseitige Formularvalidierungsbibliotheken — der Server validiert, und HTMX tauscht entweder die Erfolgsmeldung oder das Fehlerfeedback ein.

Migrationen mit Alembic

Alembic verwaltet Änderungen am Datenbankschema:

# 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

Die Autogenerate-Funktion vergleicht SQLAlchemy-Modelle mit dem aktuellen Datenbankschema und generiert Migrationsskripte. Diese Skripte sind versionierte Python-Dateien, die im Repository liegen:

# 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")

Migrationen laufen während des Deployments (bevor die Anwendung startet). Dadurch ist sichergestellt, dass das Datenbankschema zum Anwendungscode passt. Bei blakecrosley.com liegen die meisten Daten in Cloudflare D1 (Zugriff über HTTP), daher gelten Alembic-Migrationen für die lokale SQLite- oder PostgreSQL-Datenbank, die für Sitzungsdaten und Analytics verwendet wird.

Das Cloudflare D1-Muster

blakecrosley.com verwendet Cloudflare D1 als Remote-Datenbank, auf die über einen Cloudflare Worker-Proxy zugegriffen wird:

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"]

Dieses Muster eignet sich für Anwendungen, die eine Datenbank benötigen, aber keinen Datenbankserver verwalten möchten. D1 ist SQLite am Edge von Cloudflare und wird über HTTP angesprochen. Der Worker-Proxy übernimmt Authentifizierung und Rate Limiting. Der Trade-off ist Latenz: Jede Abfrage ist ein HTTP-Request (~50-100ms) statt einer lokalen Datenbankverbindung (~1-5ms). Der In-Memory-Cache beim Start mindert das bei leseintensiven Workloads wie Übersetzungen.


Sicherheit

Größe des Request-Body begrenzen

Starlette 1.6.0 hat max_body_size hinzugefügt – die Kontrolle, die diesem Stack bisher fehlte: Ohne sie kann ein Client einen unbegrenzten Body an Ihre App streamen und den Speicher zum Ausfallpunkt machen. Legen Sie den Wert für Starlette, Router, Mount oder eine einzelne Route fest oder umschließen Sie eine beliebige ASGI-App mit RequestBodyLimitMiddleware. Verschachtelte Routen können das appweite Limit erhöhen oder senken, sodass ein Upload-Endpunkt großzügig sein kann, während für alles andere enge Grenzen gelten.

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

Das Limit zählt die tatsächlich vom ASGI-Server empfangenen Bytes, einschließlich multipart-Dateidaten, und verwendet Content-Length nur für eine frühzeitige Prüfung – ein fehlender oder zu niedrig angegebener Header kann es nicht umgehen. Der Standardwert ist None, also unbegrenzt; Sie müssen diese Funktion daher ausdrücklich aktivieren.28

Middleware für Sicherheitsheader

blakecrosley.com implementiert gehärtete Sicherheitsheader über eine benutzerdefinierte 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

Die CSP enthält 'unsafe-inline' und 'unsafe-eval', weil Alpine.js sie für die Auswertung von Ausdrücken benötigt. Die Alternative ist der CSP-kompatible Build von Alpine.js, der Einschränkungen hat.14 Jede andere Funktion ist abgesichert: frame-ancestors verhindert Clickjacking, form-action beschränkt Formularübermittlungen auf denselben Origin und upgrade-insecure-requests erzwingt HTTPS.

Sicherheit des CDN-Caches mit HTMX

Die Middleware für Sicherheitsheader fügt zu Antworten von HTMX Vary: HX-Request hinzu:

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)

Ohne diesen Header könnte ein CDN eine Fragmentantwort von HTMX cachen und sie bei einer Anfrage ohne HTMX als vollständige Seite ausliefern (oder umgekehrt). Der Header Vary weist das CDN an, anhand des Werts des Headers HX-Request separate Cache-Einträge zu speichern.11

CSRF-Schutz

Formulare von HTMX verwenden zustandslose, HMAC-signierte CSRF-Token:

# 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)

Das Token wird im Template über ein globales Jinja2 generiert und in Formularanfragen von HTMX eingebunden:

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

Zustandslose Token machen serverseitigen Sitzungsspeicher überflüssig. Die HMAC-Signatur stellt sicher, dass das Token vom Server erzeugt wurde. Der Zeitstempel verhindert Replay-Angriffe. hmac.compare_digest verhindert Timing-Angriffe.15

Bereinigung von HTML

Nutzergenerierte Inhalte werden vor dem Rendern durch nh3 geleitet:

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

Die Bibliothek nh3 entfernt Tags und Attribute, die nicht auf der Allowlist stehen. Links erhalten automatisch rel="noopener noreferrer". Diese Absicherung ist unabhängig von CSP – sie verhindert gespeichertes XSS auf der Rendering-Ebene, während CSP eingeschleuste Skripte auf Browser-Ebene verhindert. Defense in depth.

Eingabevalidierung

Pydantic-Modelle validieren alle Eingaben an der Grenze von 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 gibt bei ungültigen Eingaben automatisch 422 Unprocessable Entity zurück. Zusammen mit parametrisierten Datenbankabfragen (SQLAlchemy interpoliert niemals Strings) verhindert dies SQL-Injection und gewährleistet Typsicherheit an den Schnittstellen.


Performance

Lighthouse 100/100/100/100

blakecrosley.com erreicht in allen vier Lighthouse-Kategorien 100 Punkte: Performance, Accessibility, Best Practices und SEO. Prüfen Sie dies mit PageSpeed Insights.2

Die wichtigsten Optimierungen:

Ladestrategie für CSS

blakecrosley.com lädt CSS mit einem einzelnen <link>-Tag und inhaltsgehashten URLs für unveränderliches Caching:

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

Der Helper asset() fügt einen Content-Hash (?v=a3b2c1d4) an, sodass der Browser die Datei unbegrenzt cached, bis sich ihr Inhalt ändert. Keine Extraktion von kritischem CSS, kein Print-Media-Trick, kein auf JavaScript basierendes Laden. Die CSS-Datei ist gzip-komprimiert etwa 8 KB groß – klein genug, dass der Ansatz mit einer einzelnen Anfrage bei Lighthouse Performance ohne Optimierungsakrobatik 100 Punkte erreicht.

GZip-Komprimierung

app.add_middleware(GZipMiddleware, minimum_size=500)

Antworten über 500 Bytes werden komprimiert, abzüglich der standardmäßigen Ausschlüsse für Content-Typen, die Starlette 1.5.0 eingeführt hat (Archive, Bilder, Audio, Video, Fonts, SSE). HTML komprimiert um 70–80 % und reduziert ein 15-KB-Dokument auf 3–4 KB.28

Unveränderliches Caching statischer Assets

# 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"

Statische Assets mit Content-Hash-URLs (?v=a3f8b2c1d0) werden mit immutable ein Jahr lang gecacht. Der Hash ändert sich, wenn sich die Datei ändert, und zwingt Browser und CDNs dazu, die neue Version abzurufen.

Verzögertes Laden von Skripten

<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>

Das Attribut defer lädt Skripte parallel zum Parsen von HTML herunter, führt sie jedoch erst aus, nachdem das Dokument geparst wurde. Das verhindert renderblockierendes Verhalten ohne die Komplexität von asynchronem Laden und der Verwaltung der Ausführungsreihenfolge.

Bildoptimierung

Bilder verwenden WebP mit responsivem srcset und expliziten Abmessungen:

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>

Explizite Attribute für width und height verhindern Cumulative Layout Shift (CLS). Das Attribut loading="lazy" verzögert das Laden von Bildern außerhalb des sichtbaren Bereichs. WebP bietet bei vergleichbarer Qualität 25–35 % kleinere Dateien als JPEG.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)

Der Header Link mit rel=preload weist Cloudflare an, eine 103-Early-Hints-Antwort zu senden. Dadurch kann der Browser beginnen, CSS abzurufen, bevor der Server die HTML-Antwort vollständig erzeugt hat.17

Minimales JavaScript

Der gesamte Umfang von JavaScript:

Bibliothek Größe (minifiziert + gzip-komprimiert)
HTMX ~16 KB
Alpine.js ~15 KB
Seitenspezifisches JS 4–8 KB
Gesamt 35–39 KB

Eine typische React-Anwendung liefert bereits vor dem Anwendungscode 100–300 KB Framework-JavaScript aus.18 Der Ansatz ohne Build liefert weniger JavaScript aus, weil weniger JavaScript ausgeliefert werden muss.

Deployment

Railway

blakecrosley.com wird per Git-Push auf Railway bereitgestellt:

# 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

Der Nixpacks-Builder von Railway erkennt das Python-Projekt anhand der Datei requirements.txt, installiert die Abhängigkeiten und führt den Startbefehl aus. Eine Docker-Datei ist nicht erforderlich. Der Endpunkt für Zustandsprüfungen stellt sicher, dass die Anwendung reagiert, bevor sie Datenverkehr empfängt:

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

Die Deployment-Pipeline

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

Kein npm install. Kein npm run build. Keine webpack-Kompilierung. Keine TypeScript-Kompilierung. Der einzige Installationsschritt ist pip install -r requirements.txt, dessen Ergebnis zwischen Deployments zwischengespeichert wird.

Procfile

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

Das Procfile bietet eine Heroku-kompatible Alternative. Railway unterstützt sowohl railway.toml als auch Procfile. Die Syntax ${PORT:-8000} verwendet den von der Plattform bereitgestellten Port oder standardmäßig Port 8000 für die lokale Entwicklung.

Uvicorn-Produktionskonfiguration

Verwenden Sie für Deployments mit höherem Datenverkehr mehrere Worker:

uvicorn app.main:app \
  --host 0.0.0.0 \
  --port ${PORT:-8000} \
  --workers 4 \
  --loop uvloop \
  --http httptools
  • --workers 4 startet vier Worker-Prozesse (Faustregel: 2 * CPU-Kerne + 1)
  • --loop uvloop verwendet die schnellere uvloop-Ereignisschleife (direkter Ersatz für asyncio)
  • --http httptools verwendet den schnelleren httptools-HTTP-Parser

Jeder Worker ist ein separater Prozess, der eine eigene Kopie der Anwendung im Speicher hält. Daher vervielfacht sich der Speicherbedarf pro Prozess mit der Anzahl der Worker – genau hier zahlt sich die Korrektur des Abhängigkeitsgraphen in FastAPI 0.140.0 aus: Bei einer Anwendung mit vielen Abhängigkeiten fällt mit vier Workern unter 0.139.2 der frühere Dependant-Overhead vierfach an.27

Während der Entwicklung überwacht --reload Dateiänderungen:

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

Docker-Alternative

Für Plattformen, die Docker voraussetzen:

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"]

Das schlanke Basis-Image hält den Container klein. --no-cache-dir verhindert, dass pip heruntergeladene Pakete im Image-Layer speichert.

Cloudflare-CDN

blakecrosley.com verwendet Cloudflare für CDN-Caching, DNS und 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 – Browser speichern Inhalte 5 Minuten lang im Cache
  • s-maxage=3600 – Das CDN speichert Inhalte 1 Stunde lang im Cache
  • stale-while-revalidate=86400 – Während der erneuten Validierung werden bis zu 24 Stunden lang veraltete Inhalte ausgeliefert

Statische Assets erhalten max-age=31536000, immutable, da URLs mit Inhalts-Hash die Aktualität gewährleisten.


Entscheidungsrahmen

Benötigen Sie Build-Tools?

Beantworten Sie vier Fragen:

1. Arbeiten mehr als fünf Entwickler gemeinsam an JavaScript-Schnittstellen? Falls ja, verhindert die Typprüfung von TypeScript zur Kompilierungszeit Integrationsfehler, die durch Laufzeittests erst zu spät erkannt werden. Fügen Sie einen Build-Schritt hinzu.

2. Verwaltet Ihre Anwendung einen komplexen clientseitigen Zustand? Wenn Drag-and-drop, Echtzeit-Zusammenarbeit oder Offline-First-Daten zu den Kernfunktionen gehören und nicht nur wünschenswerte Extras sind, rechtfertigt ein Framework wie React oder Svelte seine Komplexität. Fügen Sie einen Build-Schritt hinzu.

3. Nutzen mehrere Produkte eine gemeinsame Komponentenbibliothek? Falls ja, benötigt diese Bibliothek npm-Paketierung, semantische Versionierung und Tree Shaking. Fügen Sie einen Build-Schritt hinzu.

4. Sind Sie auf Bibliotheken aus dem npm-Ökosystem angewiesen, die einen Bundler voraussetzen? Wenn Radix, Framer Motion, TanStack Query oder ähnliche Bibliotheken für das Produkt unverzichtbar sind, ist eine Build-Pipeline zwingend erforderlich.

Wenn Sie alle vier Fragen mit „Nein“ beantworten, ist der Ansatz ohne Build-Prozess praktikabel. Bei jedem „Ja“ lösen Build-Tools ein reales Problem. Ein Fehler wäre es, Build-Tools hinzuzufügen, obwohl alle vier Antworten „Nein“ lauten: Damit lösen Sie Probleme, die Sie nicht haben, und schaffen gleichzeitig einen Aufwand für die Abhängigkeitsverwaltung, den Sie dann sehr wohl haben.1

Stack-Vergleich

Kategorie Ohne Build-Prozess (dieser Leitfaden) React + Build-Tools
Am besten geeignet für Inhaltsseiten, Portfolios, interne Tools, CRUD-Anwendungen SaaS-Produkte, komplexe SPAs, Nutzer von Designsystemen
Teamgröße 1–5 Entwickler 5–50+ Entwickler
Zustandsverwaltung Server (HTMX) + Client (Alpine.js) Client (React-Zustand, Redux, Zustand)
Typsicherheit Laufzeit (Pydantic auf Serverseite) Kompilierungszeit (TypeScript)
Wiederverwendung von Komponenten Jinja2-Includes + Makros npm-Pakete, gemeinsame Bibliotheken
SEO Standardmäßig serverseitig gerendert Erfordert eine SSR-/SSG-Konfiguration
Performance-Untergrenze Hoch (minimales JS, serverseitig gerendert) Variiert (Framework-Overhead)
Komplexitätsobergrenze Niedriger (kein Offline-Modus, kein komplexer clientseitiger Zustand) Höher (jede clientseitige Interaktion möglich)
Abhängigkeiten 17 Python-Pakete 300+ npm-Pakete
Build-Zeit 0 Sekunden 15–60 Sekunden

Wann HTMX die falsche Wahl ist

HTMX ersetzt den clientseitigen Zustand durch Server-Roundtrips. Das funktioniert, bis die Latenz entscheidend wird:

  • Drag-and-drop-Oberflächen – Ein Server-Roundtrip von 200 ms pro Drag-Ereignis ist nicht akzeptabel
  • Echtzeit-Zusammenarbeit – Ein durch WebSocket gesteuerter Zustand erfordert eine clientseitige Konfliktauflösung
  • Offline-First-Anwendungen – Ohne Server gibt es kein HTMX
  • Komplexe, zustandsabhängige Animationen – Framer Motion und React Spring setzen ein React-Reconciliation-Modell voraus
  • Canvas-/WebGL-Anwendungen – Die Rendering-Schleife ist grundsätzlich clientseitig

Für diese Anwendungsfälle ist ein clientseitiges Framework das richtige Werkzeug. Der Ansatz ohne Build-Prozess erhebt nicht den Anspruch, diese Frameworks zu ersetzen.


Kurzreferenz

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"

HTMX-Attribute

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) -->

Alpine.js-Attribute

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 -->

Benutzerdefinierte CSS-Eigenschaften

: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; }
}

Sicherheitsheader

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=()

Checkliste für die Projekteinrichtung

[ ] 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

FAQ

Ist HTMX für echte Webanwendungen produktionsreif?

Ja. HTMX ist seit 2020 stabil und wird branchenübergreifend in Produktionsumgebungen eingesetzt. Carson Gross, der Entwickler, betrachtet die Abwärtskompatibilität als zentrales Designprinzip — laut der HTMX-Dokumentation wird die Bibliothek bestehende Anwendungen innerhalb einer Hauptversion nicht beeinträchtigen.19 Die Bibliothek ist minimiert und gzip-komprimiert etwa 16 KB groß, hat keinerlei Abhängigkeiten und folgt der semantischen Versionierung. blakecrosley.com setzt HTMX seit drei Jahren in der Produktionsumgebung ein, ohne dass ein einziger Fehler im Zusammenhang mit HTMX aufgetreten ist.20

Kann ich TypeScript ohne Build-Schritt verwenden?

Teilweise. TypeScript-Dateien können mit tsc --noEmit typgeprüft werden, ohne Ausgabedateien zu erzeugen. Dadurch steht eine Prüfung zur Kompilierungszeit ähnlich einem Linter zur Verfügung. Browser können .ts-Dateien allerdings nicht direkt ausführen, weshalb für die Bereitstellung von TypeScript weiterhin ein Build-Schritt erforderlich ist. Alternativ lassen sich JSDoc-Typannotationen in einfachen .js-Dateien verwenden, die TypeScript ohne Kompilierung prüfen kann. So erhalten Sie während der Entwicklung Typsicherheit und liefern zugleich standardkonformes JavaScript aus.

Wie schneidet dieser Ansatz im Vergleich zu Astro oder 11ty ab?

Astro und 11ty sind Generatoren für statische Websites, die einfaches HTML mit minimalem clientseitigem JavaScript erzeugen. Sie erfordern jedoch einen Build-Schritt (Node.js, npm install und einen Build-Befehl). Beim Ansatz ohne Build entfällt dieser Schritt — der Server rendert bei jeder Anfrage HTML. Der Kompromiss: Astro und 11ty erzeugen schnellere statische Seiten, da keine serverseitige Berechnung erforderlich ist. FastAPI + HTMX verarbeitet dagegen dynamische Inhalte wie benutzerspezifische Daten, Formularübermittlungen und Echtzeitaktualisierungen nativ, ohne eine separate API-Schicht.

Wie sieht es mit serverseitigem Rendering (SSR) mit React aus?

Next.js SSR und der Ansatz mit FastAPI + HTMX verfolgen dasselbe Ziel: dem Browser serverseitig gerendertes HTML zu senden. Der Unterschied liegt darin, was nach dem anfänglichen Rendern geschieht. Next.js hydratisiert die Seite mit React und überträgt dabei die Framework-Laufzeit sowie den Komponentencode an den Client. Bei FastAPI + HTMX findet keine Hydratisierung statt — das HTML ist die endgültige Ausgabe. Nachfolgende Interaktionen verarbeitet HTMX, indem es neue HTML-Fragmente vom Server anfordert. Das Ergebnis: FastAPI + HTMX überträgt insgesamt etwa 35–40 KB JavaScript, eine Next.js-Anwendung hingegen 100–300 KB.18

Wie verarbeite ich die Formularvalidierung mit diesem Stack?

Serverseitig. Pydantic validiert die Eingaben, sobald das Formular übermittelt wird. Schlägt die Validierung fehl, gibt der Server das Formular mit Fehlermeldungen zurück. HTMX tauscht die Antwort im DOM aus:

<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
    })

Der Server validiert die Eingaben, rendert die Fehlerzustände und HTMX tauscht das Ergebnis aus. Eine clientseitige Validierungsbibliothek ist nicht erforderlich. Das HTML-Attribut required stellt als erste Verteidigungslinie eine grundlegende Validierung auf Browserebene bereit.

Kann ich Echtzeitfunktionen (WebSockets) hinzufügen?

Ja. FastAPI unterstützt WebSocket von Haus aus:

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 bietet eine WebSocket-Erweiterung (hx-ws), die Elemente mit WebSocket-Endpunkten verbindet:

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

Hinweis: HTMX 1.x verwendete die Syntax hx-ws="connect:...". In HTMX 2.x wurde die WebSocket-Unterstützung in eine separate Erweiterung (htmx-ext-ws) mit den oben gezeigten Attributen ws-connect und ws-send verlagert. Wenn Sie HTMX 1.x verwenden, funktioniert die alte hx-ws-Syntax weiterhin.

HTMX 4.0-Betaversion: htmx 4.0.0-beta6 ist jetzt über das npm-Tag next und in der 4.0-Dokumentation verfügbar (beta6 wurde am 23. Juli 2026 veröffentlicht), während der Schnellstart auf htmx.org und das npm-Tag latest weiterhin auf Version 2.0.10 verweisen. Dieser Leitfaden ist nach wie vor auf HTMX 2.x ausgerichtet. Bis Version 4.0 stabil ist, bleibt sie die empfohlene Version für den Produktionseinsatz; die Migration von 2.x auf 4.x ist ein Generationssprung und kein einfaches 2.x-Punktrelease. Das Versionierungsschema von big-skies-software überspringt ungerade Hauptversionen, weshalb 4.0 der nächste Schritt nach 2.x ist.2122

Beobachtenswerte Neuerungen der 4.0-Dokumentation. Vor der allgemeinen Verfügbarkeit von 4.0 verdienen insbesondere zwei Ergänzungen bei der Sicherheits- und Architekturprüfung Aufmerksamkeit: Die neue Erweiterung hx-live führt DOM-reaktive Ausdrücke ein, die bei Änderungen des referenzierten Zustands erneut ausgewertet werden. Die neue Erweiterung hx-nonce wiederum beschränkt die Verarbeitung von htmx-Attributen mithilfe von CSP-Nonces. Der 4.0-Migrationsleitfaden verlagert zudem mehrere Konfigurationskonzepte, stellt bestimmte Verhaltensweisen für Ereignisse und den Verlauf wieder her oder ändert sie und entfernt einige JavaScript-Hilfsfunktionen aus dem Kern. Behandeln Sie 4.0 als eigenständiges Migrationsprojekt, nicht als direkt einsetzbaren Patch für 2.x.21

Nachrichten vom Server werden mit denselben Ziel- und Austauschmechanismen wie HTTP-Antworten in den DOM eingefügt. Der Server sendet HTML-Fragmente über die WebSocket, anschließend fügt HTMX sie ein.

Wie handhabt dieser Stack SEO?

Serverseitig gerendertes HTML ist von Natur aus SEO-freundlich, da Crawler den vollständigen Seiteninhalt erhalten, ohne JavaScript ausführen zu müssen. blakecrosley.com ergänzt mehrere SEO-Ebenen:

  • Strukturierte JSON-LD-Daten im <head> jeder Seite (Schemas Person, Article, WebSite und FAQPage)
  • Dynamische Sitemap mit hreflang-Alternativen für alle 10 Gebietsschemas
  • RSS-Feed unter /blog/feed.xml
  • llms.txt im Stammverzeichnis, damit AI-Crawler die Website leichter finden können
  • Kanonische URLs und Open-Graph-Tags in der Basisvorlage
  • Semantisches HTML: <article>, <section>, <main> und eine korrekte Überschriftenhierarchie

Es ist keine SSR-Konfiguration erforderlich. Kein getStaticProps. Kein ISR. Das HTML wird bei jeder Anfrage gerendert — das ist das Standardverhalten und keine Optimierung.

Wie steil ist die Lernkurve im Vergleich zu React?

Für Python-Entwickler ist die Lernkurve erheblich flacher. Sie kennen die Sprache bereits. Die Route-Handler von FastAPI geben Vorlagenantworten zurück — dasselbe mentale Modell wie bei Views in Flask oder Django. HTMX ergänzt einige wenige HTML-Attribute (hx-get, hx-target, hx-swap). Alpine.js fügt einige weitere hinzu (x-data, x-show, @click). Sie müssen weder JSX noch einen virtuellen DOM, ein Hook-System, eine Zustandsverwaltungsbibliothek oder die Konfiguration eines Build-Werkzeugs erlernen.

Die HTMX-Dokumentation passt auf eine einzige lange Seite. Die Alpine.js-Dokumentation umfasst nur wenige Seiten. Die React-Dokumentation erstreckt sich über Hunderte Seiten und behandelt Hooks, Context, Refs, Effects, Suspense, Server Components und Streaming SSR.

Für JavaScript-/React-Entwickler ist die Umstellung eher konzeptioneller als syntaktischer Natur. Die zentrale Erkenntnis lautet: Der Server verwaltet den Zustand und rendert das HTML. Aus clientseitiger Zustandsverwaltung wird serverseitige Route-Verarbeitung. An die Stelle des clientseitigen Datenabrufs treten HTMX-Attribute auf HTML-Elementen. Die Syntax ist einfacher — beim mentalen Modell müssen Sie sich jedoch von der SPA-Annahme lösen, dass der Client für das Rendering zuständig ist.


Änderungsprotokoll

Datum Änderung Quelle
2026-08-16 Starlette 1.3.1 → 1.6.0, und eine der Änderungen verändert unbemerkt den eigenen GZip-Ausschnitt dieses Leitfadens. Verhaltensänderung: Starlette 1.5.0 (8. Aug.) hat DEFAULT_EXCLUDED_CONTENT_TYPES weit über text/event-stream hinaus erweitert und umfasst nun gzip/zip-Archive, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/* sowie WOFF/WOFF2-Schriftarten. Diese werden daher nicht mehr standardmäßig komprimiert; image/* ist bewusst nicht ausgeschlossen, sodass image/svg+xml komprimierbar bleibt. Ein neues keyword-only-Argument exclude_content_types überschreibt die Liste, der Abgleich erfolgt ohne Beachtung der Groß-/Kleinschreibung, und eine Neuzuweisung der Modուլkonstante zur Laufzeit wirkt nicht mehr. Die Laufzeit-Pin von FastAPI lautet starlette>=0.46.0 ohne Obergrenze, sodass eine frische Installation 1.6.0 bezieht und die Änderung ohne Zutun der Leser auf den Ausschnitt in diesem Leitfaden wirkt — beide GZip-Passagen wurden korrigiert. 1.5.0 überspringt außerdem Status-206-Teilantworten und leert bei jedem gestreamten Chunk; 1.4.0 (5. Aug.) verlagert gzip-Chunks ab einer thread_minimum_size von 128 KiB in einen Worker-Thread, sodass große Komprimierungen den Event Loop nicht mehr blockieren. Neue Möglichkeit: 1.6.0 (8. Aug.) ergänzt max_body_size bei Starlette/Router/Mount/Route sowie RequestBodyLimitMiddleware — ein neuer Security-Unterabschnitt behandelt dies, da eine Begrenzung des Request-Body in diesem Leitfaden bisher vollständig fehlte. Ebenfalls vermerkt: encode/starlette leitet nun zu Kludex/starlette weiter und folgt damit dem Uvicorn-Wechsel. Nur für das Änderungsprotokoll: Uvicorn 0.52.0–0.52.3 (experimentelle Zig-basierte zttp-HTTP/1.1-Implementierung, von deren Einsatz vor Production-Traffic selbst im Release abgeraten wird; der Rat des Leitfadens zu --http httptools bleibt bestehen), Alpine.js 3.16.0/3.16.1, SQLAlchemy 2.0.52 (Python-3.15-Unterstützung; Korrektur einer Fehlausrichtung von Ergebnisspalten bei ORM UPDATE mit synchronize_session="fetch"). Unverändert verifiziert: 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. Im Zeitraum gab es bei allen neun Abhängigkeiten keine neuen Security Advisories. 28
2026-07-29 FastAPI 0.141.0 + 0.141.1 (beide am 29. Juli). 0.141.0 ergänzt app.frontend(check_dir="auto"), sodass fastapi dev nicht mehr fehlschlägt, wenn das Build-Verzeichnis fehlt — der normale Fall, wenn der Server vor dem Frontend-Build gestartet wird. 0.141.1 korrigiert wenige Stunden später Abhängigkeiten in app.frontend(), bei denen Background Tasks und Response Headers verloren gingen; setzte eine Abhängigkeit ein Cookie oder plante eine BackgroundTask, wurde diese Arbeit beim Frontend-Mount verworfen, während sie auf API-Routen korrekt funktionierte. Damit wird eine echte Lücke in der mit 0.139.0 eingeführten Abhängigkeitsunterstützung geschlossen. Beide Änderungen gehören in die bestehende app.frontend()-Erläuterung statt in einen neuen Abschnitt, da der servergerenderte Ansatz dieses Leitfadens kein dist/-Verzeichnis mountet. 0.141.1 dokumentiert außerdem FASTAPI_ENV im FastAPI-CLI-Leitfaden (nur Dokumentation, keine Änderung im Text). 29
2026-07-27 FastAPI veröffentlichte am 27. Juli innerhalb von fünfeinhalb Stunden die Versionen 0.140.1 bis 0.140.7 — sieben Releases, allesamt Refactorings der Abhängigkeitsmechanik, die mit 0.140.0 begann. Zwei Stränge: Die flache Kopie des Abhängigkeitsgraphen, die FastAPI bisher aufbaute und vorhielt, ist nun entfernt (0.140.2), ebenso jede verbliebene Stelle, die eine solche Kopie neu aufbaute — OpenAPI-Generierung (0.140.3, 0.140.7), Body-Felder (0.140.5), Request-Parameter (0.140.6) — und 0.140.4 entfernt ungenutzte Buchhaltung zur Wiederholungsverfolgung. Die einzige Änderung mit sichtbarem Schwellenwert ist 0.140.1: Der lru_cache der Hilfsfunktionen zur Callable-Klassifizierung in fastapi/dependencies/models.py steigt von 1.024 auf 4.096 Einträge (mit dem Namen _CALLABLE_CLASSIFICATION_CACHE_SIZE), nachdem Apps mit mehr als 1.024 unterschiedlichen Abhängigkeiten den Cache überlasteten. Keine API-Änderungen; die Empfehlung im Absatz zum Abhängigkeitsspeicher wechselt von 0.140.0 zu 0.140.7 oder neuer, mit einem Hinweis darauf, dass sich die Reihe weiterhin bewegt und die OpenAPI-Abhängigkeits-Benchmarks (PR #16075) erst im letzten Release der Serie gelandet sind. 30
2026-07-25 FastAPI 0.140.0 (24. Juli, 21:16 UTC) behebt eine Speicherregression im Abhängigkeitssystem, die seit 0.121.0 (3. November 2025) vorhanden war. PR #16049 entfernt zehn Attribute vom Typ functools.cached_property aus Dependant, verschiebt sie in Hilfsfunktionen auf Modulebene und macht die Klasse zu @dataclass(slots=True); der offizielle CodSpeed-Lauf des gemergten PR meldet für den Speicherbenchmark test_dependency_graph 17,5 MB → 1,1 MB (×16), und der ursprüngliche Bericht beschrieb einen Production-OOM unter 0.121.3, während 0.120.4 unter etwa 400 MB blieb. Ein Abschnitt zu 0.140.0 wurde zu Async Patterns ergänzt und eine Zeile zum Worker-Speicher zu Uvicorn Production Configuration. Außerdem wurde ein bestehender Fehler korrigiert: Der Leitfaden behauptete, 0.137.0 „pinnt Starlette auf die 1.x-Reihe“ — das tut es nicht. Die Laufzeitanforderung von FastAPI ist starlette>=0.46.0 (eine Untergrenze ohne Obergrenze, die weiterhin durch Starlette 0.4x erfüllt wird) einheitlich in 0.136.3, 0.137.0, 0.138.0, 0.139.2 und 0.140.0; die 1.x-Nummern in den 0.137.0-Hinweisen sind dependabot-Bumps für die Test-Lockfile des Repositories (PR #15722 betrifft ausschließlich uv.lock). Aussage im Text und 24 wurden beide korrigiert. Zwei ausdrücklich markierte Nicht-Änderungen: Dependant-Interna sind nun für Tooling inkompatibel (oauth_scopes, cache_key, _uses_scopes, _is_security_scheme existieren nicht mehr als Attribute, sondern wurden durch die Modulfunktionen _get_oauth_scopes() / _get_cache_key() / _uses_scopes() ersetzt, und slots=True verhindert Instance-Monkey-Patching) — undokumentiertes internes API, auf das dieser Leitfaden nie verweist, dieselbe Kategorie wie die Änderung von router.routes in 0.137.0; außerdem verwenden die offiziellen Dokumente von FastAPI nun standardmäßig uv-Projekte anstelle von pip/venv in 30 Dateien, darunter README, index.md, virtual-environments.md und die Docker/Deployment-Seiten (PR #16032, gemergt am 21. Juli). Die Dokumentationsänderung ist für Ihren Code kosmetisch, aber der Leitfaden lehrt durchgehend pip install -r requirements.txt und weicht damit nun vom Einstieg von Upstream ab — eine künftige redaktionelle Entscheidung, die bei diesem Durchgang bewusst nicht getroffen wurde. 27
2026-07-24 htmx 4.0.0-beta6 ersetzt beta5 als npm-Tag next (veröffentlicht am 23. Juli 2026; GitHub-Release am selben Tag). Zu den wichtigsten Beta-Änderungen gehören: die neue Erweiterung hx-multipart (gestreamte multipart/mixed/multipart/parallel-Responses mit HX-*-Action-Headers pro Teil), Wiederherstellung der History-Scrollposition über die Navigation API mit Firefox-Fallback, die Umbenennung des Beta-internen Events htmx:swap:finallyhtmx:finally:swap, HX-Trigger-Response-Header-Events werden nun nach dem Swap ausgelöst, benutzerdefinierte Request-Methoden und ein Rewrite von hx-ws mit Weitergabe von protocols. Die Empfehlung bleibt unverändert — Production bleibt bei HTMX 2.x (latest = 2.0.10), bis 4.0 GA erreicht; die Umbenennung ist nur innerhalb der 4.0-Beta-Reihe Breaking. Hinweis zum Beta-Track und 21 wurden aktualisiert. FastAPI 0.139.2, Uvicorn 0.51.0, Alpine.js 3.15.12, Starlette 1.3.1, Jinja2 3.1.6 wurden alle als unverändert verifiziert; im Zeitraum gab es keine Security Advisories.
2026-07-17 FastAPI 0.139.1 + 0.139.2 (16. Juli): Korrektur für Punktpfade bei app.frontend()-Fallbacks (/users/john.doe, PR #16011) und threadsicheres Erstellen von Router-Routen für Parallel-Thread-Tests (PR #16013) — keine anwendungsseitige API-Änderung. Uvicorn 0.49.0 → 0.51.0: Die ältere websockets-Implementierung ist deprecated, und auto verwendet nun standardmäßig websockets-sansio (0.50.0), die Standardimplementierung erfordert websockets>=13.0 (0.50.2), und 0.51.0 (8. Juli) ergänzt SIGHUP-Worker-Neustarts mit Überlappung für Reloads nahezu ohne Downtime; das Repository liegt nun bei 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 wurden alle als unverändert verifiziert; im Zeitraum gab es keine Security Advisories.
2026-07-07 htmx 4.0.0-beta5 ist nun der npm-Tag next (veröffentlicht am 26. Juni 2026) und ersetzt beta4; der Hinweis zum HTMX-4.0-Beta-Track und [^22] wurden entsprechend aktualisiert. Die Empfehlung bleibt unverändert — Production-Arbeit bleibt bei HTMX 2.x (latest = 2.0.10), bis 4.0 GA erreicht. Gegen die npm-dist-tags von htmx.org verifiziert.
2026-07-02 FastAPI 0.139.0 (1. Juli). app.frontend() unterstützt nun dependencies — beispielsweise automatische Cookie-Authentifizierung für das bereitgestellte Frontend (PR #15908) — und erweitert den statischen Frontend-Mount aus 0.138.0 um die übliche Depends()-Mechanik; weiterhin orthogonal zur servergerenderten These dieses Leitfadens und im selben Kontrastabsatz vermerkt. Keine weiteren Änderungen im Stack: HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8, SQLAlchemy 2.0.51 unverändert. 26
2026-06-22 FastAPI 0.138.0 + 0.137.2. 0.138.0 (20. Juni) ergänzt app.frontend("/", directory="dist") / router.frontend(...) zum Bereitstellen eines gebauten statischen Frontends (SPA-dist/-Output) — orthogonal zur servergerenderten No-Build-These dieses Leitfadens, als Kontrast im Abschnitt Async Patterns vermerkt. 0.137.2 (18. Juni) ergänzt iter_route_contexts() als unterstützte Methode zum Auflisten von Routen, nachdem router.routes intern ist (seit 0.137.0). Beide sind Funktionserweiterungen ohne Breaking Changes; 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) alle unverändert. 25
2026-06-16 FastAPI 0.137.0/0.137.1 + Starlette 1.0→1.3.1. FastAPI 0.137.0 (14. Juni) refaktoriert Router-Interna: router.routes ist nun ein interner Baum und keine flache Liste von APIRoute mehr (Breaking für alles, was darüber iteriert), ermöglicht jedoch Routen, die nach include_router() hinzugefügt werden, sowie neue Hooks APIRouter.matches()/.handle(); 0.137.1 (15. Juni) korrigiert das APIRoute-Typing und prefixlose Router mit leerem Pfad. Starlette veröffentlichte am 22. März sein erstes stabiles 1.0 und liegt nun bei 1.3.1 (12. Juni); dabei wurden die deprecated Hooks on_event/on_startup/on_shutdown sowie die Decorators @app.route()/@app.websocket_route() entfernt — lifespan und Route/WebSocketRoute sind die einzigen Wege. (Dieser Eintrag behauptete ursprünglich, dass FastAPI 0.137.0 Starlette 1.3.1 pinnt — korrigiert am 2026-07-25: Das tut es nicht; die Laufzeitanforderung lautet starlette>=0.46.0 ohne Obergrenze.) Ein Hinweis zu Lifespan/Router wurde zum Abschnitt Async Patterns ergänzt. SQLAlchemy 2.0.51 (15. Juni) enthält ausschließlich Bugfixes. 24
2026-06-08 Änderung der SQLAlchemy-2.0.50-Async-Installation. Seit SQLAlchemy 2.0.50 wird die greenlet-Abhängigkeit des Async-Stacks nicht mehr standardmäßig installiert — installieren Sie das Extra sqlalchemy[asyncio] (andernfalls schlägt das erste await gegen die Engine mit einem Missing-Greenlet-Fehler fehl). 2.0.50 erfordert außerdem Python 3.10+ (3.7–3.9 entfallen) und ergänzt Wheels für Free-Threaded 3.13t. Ein Installationshinweis wurde zum SQLAlchemy-2.0-Async-Abschnitt ergänzt. Keine Textänderung für den übrigen Stack: Die neueste Version von FastAPI ist weiterhin 0.136.3 (2026-05-23, kein Juni-Release), htmx stable bleibt 2.0.10 (4.0.0-beta4 „The Fetchening“ ist in Beta mit einem angestrebten Stable-Release um Anfang 2027 und noch keine Production-Empfehlung), Alpine.js 3.15.12, Bootstrap 5.3.x unverändert. Die Production-Empfehlung bleibt unverändert: HTMX 2.x bis 4.0 stable ist.23
2026-05-24 Wartungsprüfung: Das lokale Content-Inventar zeigt weiterhin 210 Blogbeiträge, 11 Kernleitfäden, 48 Designstudien und 10 unterstützte Locales einschließlich Englisch. Die neueste Version von FastAPI ist 0.136.3 (2026-05-23); das einzige in den Release Notes hervorgehobene anwendungsseitige Refactoring ist die strengere Behandlung von Unterstrich-Headers bei convert_underscores=True, und 0.136.2 validiert Server-Sent-Event-Felder, um fehlerhafte Event-Daten zu vermeiden. htmx stable bleibt 2.0.10, während npm next und die 4.0-Dokumentation nun auf 4.0.0-beta4 verweisen; die neueste SQLAlchemy-2.0-Version ist 2.0.50; Pydantic latest bleibt 2.13.4. Die Production-Empfehlung bleibt unverändert: Verwenden Sie HTMX 2.x, bis 4.0 stable wird.122
2026-05-18 Aktualisierung des Site-Inventars: Das lokale Content-Inventar zeigt nun 210 Blogbeiträge, 11 Kernleitfäden, 48 Designstudien und 10 unterstützte Locales einschließlich Englisch. Die neueste Version von FastAPI bleibt 0.136.1; htmx stable bleibt 2.0.10 mit npm next auf 4.0.0-beta3; Alpine.js npm latest bleibt 3.15.12. Die Production-Empfehlung bleibt unverändert: Verwenden Sie HTMX 2.x, bis 4.0 stable wird.12021
2026-05-15 Wartungsprüfung: Die neueste Version von FastAPI bleibt 0.136.1; diese lokale Site-Umgebung importiert FastAPI 0.128.0 und Starlette 0.50.0; htmx stable bleibt 2.0.10 und npm next ist nun 4.0.0-beta3; Alpine.js npm latest ist 3.15.12; Bootstrap latest ist 5.3.8; SQLAlchemy 2.0 latest ist 2.0.49; Pydantic latest ist 2.13.4. Die Production-Empfehlung bleibt unverändert: Verwenden Sie HTMX 2.x, bis 4.0 stable wird.2021
2026-05-09 Tracking von htmx 4.0.0-beta3 (8. Mai 2026): htmx 4.0.0-beta3 ist über den npm-Tag next und in der 4.0-Dokumentation verfügbar, während npm latest bei 2.0.10 bleibt. Vor GA beobachtenswerte Highlights: die neue Erweiterung hx-live (DOM-reaktive Ausdrücke), die neue Erweiterung hx-nonce (CSP-Nonce-Schutz für htmx-Attribute) und Änderungen im Migrationsleitfaden an Konfiguration, History, Events und den zentralen JavaScript-Hilfsfunktionen. Die Production-Empfehlung bleibt unverändert: htmx 2.x bleibt der neueste npm-Tag und die empfohlene Version bis 4.0 GA.21
2026-05-07 Wartungsprüfung: Die neueste Version von FastAPI bleibt 0.136.1; htmx stable ist 2.0.10 und v4 bleibt Beta mit einem Ziel im Sommer ’26; Alpine.js npm latest ist 3.15.12; Bootstrap latest ist 5.3.8; SQLAlchemy 2.0 latest ist 2.0.49; Pydantic latest ist 2.13.4. Die lokalen Site-Metriken wurden auf 182 Blogbeiträge, 11 Leitfäden, zehn unterstützte Locales und 17 Python-Anforderungen aktualisiert. Die Migrationsleitlinien bleiben unverändert: Verwenden Sie HTMX 2.x für Production, bis 4.0 stable wird.20
2026-04-25 FastAPI 0.136.1 (23. April 2026): Bereinigung von Pydantic-v2-Deprecations (keine Verhaltensänderungen für Anwendungscode). Zeitplan für HTMX 4.0 verfolgt: htmx 4.0.0-beta1 (6. Apr.) und 4.0.0-beta2 (14. Apr.) wurden veröffentlicht. Migrationsleitlinien unverändert — htmx 2.x bleibt auf dem neuesten npm-Tag, bis 4.0 stable ist; Security Fixes gehen weiter, kein Upgrade-Druck. Wichtige 4.0-Änderungen, die Sie jetzt beim Design berücksichtigen sollten: (1) fetch() ersetzt XMLHttpRequest als zentrale Ajax-Infrastruktur, (2) Attributvererbung wird standardmäßig explizit, (3) die History-Unterstützung stellt eine Netzwerkanfrage für wiederhergestellte Inhalte (kein lokaler DOM-Snapshot). FastAPI 0.135.4 (16. April) entfernte den April-Fool-Decorator @app.vibe(), der in 0.135.3 hinzugekommen war.
2026-04-16 HTMX-4.0-beta-Bewusstsein ergänzt (Forward Reference). Unterstützung von FastAPI 0.136.0 für Python-3.14t-Free-Threaded-Builds vermerkt. Pydantic-2.13.x-Funktionen (Default Factories für private Attribute mit Zugriff auf validierte Model-Daten, pydantic.v1-Namespace auf 1.10.26 mit 3.14-Unterstützung). Alpine.js-3.15.11-Fixes: Modifier x-anchor.noflip, Warnung für mehrere Root-Elemente bei x-for, Regression bei $refs morph korrigiert.
2026-03-24 Erstveröffentlichung

Referenzen


Dieser Leitfaden behandelt das vollständige System, das zum Erstellen von blakecrosley.com verwendet wird. The No-Build Manifesto liefert die philosophische Begründung. Der Beitrag Lighthouse Perfect Score dokumentiert den Weg der Performance-Optimierung. Der Beitrag Vibe Coding vs. Engineering untersucht, wo KI-gestützte Entwicklung in diesen Workflow passt.


  1. Produktionsmetriken von blakecrosley.com vom 18. Mai 2026. Die Website umfasst 210 Blogbeiträge, interaktive JavaScript-Komponenten, 11 Kernleitfäden, 48 Designstudien, Englisch sowie 9 übersetzte Sprachversionen, minimale Python-Abhängigkeiten und keine Build-Tools. Überprüft anhand des lokalen Inhaltsinventars, app/i18n/config.py und requirements.txt

  2. Google PageSpeed Insights (pagespeed.web.dev) führt Lighthouse-Audits für jede öffentliche URL aus. blakecrosley.com erzielt seit März 2026 100/100/100/100 (Performance, Accessibility, Best Practices, SEO). Die Ergebnisse sind öffentlich überprüfbar. Die vollständige Optimierungsreise finden Sie unter From 76 to 100: Achieving a Perfect Lighthouse Score

  3. Ein frisches npx create-next-app@latest (Next.js 15, getestet im Februar 2026) installiert 311 Pakete in node_modules/ mit insgesamt 187 MB. Produktionsprojekte mit zusätzlichen Abhängigkeiten liegen tendenziell höher. Einzelne Projekte unterscheiden sich. Quelle: Tests des Autors, dokumentiert in The No-Build Manifesto

  4. Die Next.js-Performance-Dokumentation von Vercel empfiehlt konkrete Optimierungen (Bildoptimierung, Schriftarten-Laden, Code Splitting), um Werte über 90 zu erreichen. Siehe nextjs.org/docs/app/building-your-application/optimizing. Der Bereich von 70–90 spiegelt die Standardeinstellungen wider, bevor diese Optimierungen angewendet werden. 

  5. Vollständige Abhängigkeitsliste, überprüft anhand von requirements.txt von blakecrosley.com im Mai 2026. Die Datei enthält derzeit 17 Python-Anforderungseinträge und keine Build-Tools, Compiler oder Bundler. 

  6. Nach Erfahrung des Autors bei der Pflege von Next.js-Projekten (2021–2024) erzeugt das JavaScript-Ökosystem bei aktiven Projekten 15–25 Dependabot-PRs pro Monat. Die meisten aktualisieren transitive Abhängigkeiten, die der Entwickler nie direkt importiert hat. 

  7. Tim Berners-Lee formulierte Abwärtskompatibilität als Prinzip des Webdesigns: „Ein Browser sollte abwärtskompatibel sein.“ Eine Seite aus dem Jahr 1996 wird in Chrome 2026 dargestellt. Siehe w3.org/DesignIssues/Principles

  8. OWASP empfiehlt, API-Dokumentationsendpunkte in der Produktion zu deaktivieren, um die Angriffsfläche zu reduzieren. Der Endpunkt /openapi.json legt alle Routendefinitionen, Parameter und Antwortmodelle offen. 

  9. FastAPI-Dokumentation zu async- und sync-Handlern: fastapi.tiangolo.com/async/. Wenn await in async-Funktionen mit blockierenden Aufrufen kombiniert wird, wird die Event Loop ausgehungert. 

  10. nh3 ist ein Rust-basierter HTML-Sanitizer und der Nachfolger der Bleach-Bibliothek. Er wird vom PyO3-Projekt gepflegt und bietet allowlist-basierte HTML-Sanitisierung. Siehe github.com/messense/nh3

  11. Der Header Vary ist in RFC 9110, Abschnitt 12.5.5, definiert. Er weist Caches an, separate Antworten anhand der angegebenen Werte von Request-Headern zu speichern. Ohne Vary: HX-Request könnte ein CDN ein HTMX-Fragment als vollständige Seitenantwort ausliefern. Siehe httpwg.org/specs/rfc9110.html#field.vary

  12. CSS Custom Properties (CSS Variables) werden von mehr als 97 % der globalen Browser unterstützt. Sie kaskadieren, werden vererbt und reagieren zur Laufzeit auf Media Queries – Fähigkeiten, die Präprozessor-Variablen fehlen. Quelle: caniuse.com/css-variables

  13. Googles hreflang-Dokumentation: developers.google.com/search/docs/specialty/international/localized-versions. Der Wert x-default kennzeichnet die Fallback-Seite für Benutzer, deren Sprache nicht in der hreflang-Liste enthalten ist. 

  14. Alpine.js benötigt 'unsafe-eval' in der Content Security Policy für seine Engine zur Auswertung von Ausdrücken. Der CSP-kompatible Build (@alpinejs/csp) vermeidet diese Anforderung, hat jedoch Einschränkungen. Siehe alpinejs.dev/advanced/csp

  15. HMAC-basierte CSRF-Tokens folgen dem Muster „Signed Double-Submit Cookie“, das im OWASP CSRF Prevention Cheat Sheet beschrieben wird. hmac.compare_digest verwendet einen Vergleich mit konstanter Laufzeit, um Timing-Side-Channel-Angriffe zu verhindern. Siehe cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

  16. WebP bietet bei vergleichbarer visueller Qualität 25–35 % kleinere Dateien als JPEG. Googles WebP-Studie: developers.google.com/speed/webp/docs/webp_study

  17. 103 Early Hints ermöglicht es dem Server (oder CDN), eine vorläufige Antwort mit Preload-Hinweisen zu senden, bevor die endgültige Antwort bereitsteht. Cloudflare unterstützt Early Hints für Link-Header mit rel=preload. Siehe developer.chrome.com/blog/early-hints

  18. React 18 + ReactDOM wiegt ungefähr 42 KB minifiziert und gzip-komprimiert. Mit einem Router, einer Bibliothek zur Zustandsverwaltung und der Runtime eines Build-Frameworks liefern typische React-Anwendungen 100–300 KB Framework-JavaScript aus. Quelle: bundlephobia.com/package/react-dom@18.2.0

  19. Die Richtlinie zur Versionierung und das Bekenntnis zur Abwärtskompatibilität von HTMX sind unter htmx.org/migration-guide-htmx-1/ dokumentiert. Carson Gross formulierte das Prinzip der Abwärtskompatibilität in Hypermedia Systems (2023) von Gross, Stepinski und Cotter: hypermedia.systems

  20. Wartungsprüfung vom 15. Mai 2026. FastAPI PyPI und die Release Notes führen 0.136.1 auf; die lokale Importprüfung ergab FastAPI 0.128.0 und Starlette 0.50.0 für die Website-Umgebung; htmx.org führt 2.0.10 im Quick Start auf; npm view htmx.org version dist-tags ergab latest=2.0.10 und next=4.0.0-beta3; npm view alpinejs version und npm view @alpinejs/csp version ergaben 3.15.12; der offizielle Bootstrap-Blog und die npm-Paketmetadaten führen 5.3.8 auf; SQLAlchemy-PyPI und die Dokumentation führen 2.0.49 auf; Pydantic-PyPI führt 2.13.4 auf. 

  21. htmx 4.0.0-beta6 ist das aktuelle npm-Tag next (veröffentlicht am 23. Juli 2026; die Beta-Reihe verlief von beta3 am 8. Mai 2026 → beta4 → beta5 → beta6), während npm latest weiterhin 2.0.10 ist. Die 4.0-Dokumentation unter four.htmx.org verfolgt den Build next, der Index der 4.0-Erweiterungen führt hx-live und hx-nonce auf, und der 4.0-Migrationsleitfaden dokumentiert Änderungen für die Migration, die vor dem Umstieg von Produktionsanwendungen von 2.x geprüft werden sollten. Abgleich mit den npm-dist-tags von htmx.org am 24. Juli 2026. 

  22. Wartungsprüfung vom 24. Mai 2026. Lokale Inventarisierungsbefehle ergaben 210 Markdown-Blogbeiträge, 11 Leitfadendateien auf oberster Ebene und 48 Designstudien-Dateien. FastAPI Release Notes führen 0.136.3 am 23. Mai 2026 mit strengerer Behandlung von Unterstrich-Headern bei convert_underscores=True auf; 0.136.2 validiert Server-Sent-Event-Felder. python3 -m pip index versions fastapi ergab als neueste Version 0.136.3; python3 -m pip index versions sqlalchemy ergab als neueste Version 2.0.50; python3 -m pip index versions pydantic ergab als neueste Version 2.13.4. npm view htmx.org dist-tags version time.modified --json ergab latest=2.0.10, next=4.0.0-beta4 und time.modified=2026-05-22T15:56:21.948Z; die Installationsdokumentation von four.htmx.org zeigt htmx.org@4.0.0-beta4

  23. SQLAlchemy-2.0.50-Changelog und Release-Blog, veröffentlicht am 24. Mai 2026. Die asyncio-Abhängigkeit greenlet wird nicht mehr standardmäßig installiert; das Installationsziel sqlalchemy[asyncio] ist nun erforderlich, um sie einzubinden. 2.0.50 stellt außerdem die Unterstützung für Python 3.7/3.8/3.9 ein (nun 3.10+), ergänzt Free-Threaded-Python-Wheels und fügt einen Window-Frame-Parameter over(..., exclude=...) hinzu. Neueste Version am 8. Juni 2026 auf PyPI überprüft. htmx 4.0.0-beta4 („The Fetchening“, 22. Mai 2026) bleibt Beta mit einem stabilen Ziel Anfang 2027; FastAPI 0.136.3 (23. Mai 2026), Alpine.js 3.15.12 und Bootstrap 5.3.x sind in diesem Zeitraum unverändert. 

  24. FastAPI Release Notes: 0.137.0 (14. Juni 2026) überarbeitet Router-Interna, sodass router.routes keine flache Liste von APIRoute-Objekten mehr ist, sondern ein Baum aus Zwischenobjekten (als intern behandeln); außerdem ermöglicht es das Hinzufügen von Routen nach include_router(), einschließlich eines Sub-Routers, bevor dessen Routen definiert sind, vermeidet das Kopieren von Routen und ergänzt APIRouter.matches()/.handle(). Es legt Starlette nicht auf 1.x fest: Die Laufzeitanforderung von FastAPI lautet starlette>=0.46.0 – eine Untergrenze ohne Obergrenze – und ist identisch in 0.136.3, 0.137.0, 0.138.0, 0.139.2 und 0.140.0, überprüft anhand der Metadaten requires_dist im PyPI-JSON-API am 25. Juli 2026. Die Zeile „bump starlette from 1.1.0 to 1.2.1“ (PR #15722) in den Anmerkungen zu 0.137.0 ist ein Dependabot-Update unter Internal, das nur die Test-Lockdatei uv.lock des Repositorys betrifft. (Eine Obergrenze bestand zuvor – 0.120.4 und 0.121.0 enthielten starlette<0.50.0,>=0.40.0 –, wurde jedoch mit 0.136.3 entfernt.) Korrektur am 25. Juli 2026 vorgenommen; die vorherige Formulierung dieser Fußnote und die Aussage im Haupttext waren falsch. 0.137.1 (15. Juni 2026) korrigiert die Typisierung von APIRoute und einen leeren Pfad in einem Router ohne Präfix. Starlette-Release Notes: 1.0.0 (22. März 2026), die erste stabile Veröffentlichung seit etwa 8 Jahren, entfernte on_startup/on_shutdown/on_event() und die Decorators @app.route()/@app.websocket_route() (verwenden Sie lifespan und Route/WebSocketRoute); die neueste Version ist 1.3.1 (12. Juni 2026). SQLAlchemy 2.0.51 (Changelog, 15. Juni 2026) enthält ausschließlich Fehlerbehebungen ohne Auswirkungen auf async oder die Installation. Über PyPI und offizielle Release Notes am 16. Juni 2026 überprüft. 

  25. FastAPI Release Notes: 0.138.0 (20. Juni 2026) ergänzt app.frontend("/", directory="dist") und router.frontend("/", directory="dist"), um ein gebautes statisches Frontend auszuliefern (PR #15800; Frontend-Dokumentation) – eine statische dist/-SPA-Auslieferungsfunktion, kein servergerendertes Muster; keine Breaking Change. 0.137.2 (18. Juni 2026) ergänzt iter_route_contexts() für fortgeschrittene Nutzung, die zuvor router.routes durchlief (seit 0.137.0 intern); keine Breaking Change. Stand 22. Juni 2026 gab es keine neuere Veröffentlichung als 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 und Bootstrap 5.3.8 sind alle unverändert. Über PyPI und offizielle Release Notes am 22. Juni 2026 überprüft. 

  26. FastAPI Release Notes zu 0.139.0, 1. Juli 2026: „Unterstützung für Abhängigkeiten in app.frontend(), z. B. für automatische Cookie-Authentifizierung für das Frontend“ (PR #15908). Der Rest der Veröffentlichung besteht aus Übersetzungen, Dokumentation und Abhängigkeitsupdates; keine Breaking Changes. Überprüfung in der aktuellen Sitzung am 2. Juli 2026 (PST): 0.139.0 ist die neueste Veröffentlichung auf der GitHub-Release-Seite. 

  27. FastAPI Release Notes zu 0.140.0, veröffentlicht am 24. Juli 2026 um 21:16 UTC (PyPI upload_time_iso_8601 2026-07-24T21:16:42Z). Der einzige Refactoring-Eintrag lautet „⚡️ Reduce memory usage in dependencies. PR #16049“ (gemergt am 24. Juli 2026 um 21:07:52 UTC). Die Regression wurde durch PR #14262 eingeführt (gemergt am 3. November 2025), noch am selben Tag in 0.121.0 veröffentlicht und ergänzte functools.cached_property zu Dependant.cache_key; in 0.139.2 enthielt die Klasse zehn Definitionen mit @cached_property. In 0.140.0 deklariert fastapi/dependencies/models.py @dataclass(slots=True) class Dependant, wobei die Logik in die Funktionen auf Modulebene _get_cache_key(), _get_oauth_scopes(), _uses_scopes() und _is_security_scheme() verlagert wurde – Quellcode unter Tag 0.140.0 überprüft. Der CodSpeed-Bot im gemergten PR meldet für den Speicher-Benchmark test_dependency_graph 17,5 MB (base) → 1,1 MB (head), „improve performance by ×16“; 0.140.0 ergänzt außerdem einen CI-Speicher-Benchmark (PR #16046), damit dies nicht erneut regressiert. Der ursprüngliche Bericht ist Diskussion #14742, in der 0.120.4 unter etwa 400 MB blieb und 0.121.3 in der Produktion einen OOM verursachte. Hinweis für Autoren von Tools: Dependant.oauth_scopes, .cache_key, ._uses_scopes und ._is_security_scheme existieren nicht mehr als Attribute, und slots=True verhindert Monkey-Patching von Instanzen – ein undokumentiertes internes API, das dieser Leitfaden nicht verwendet und das in dieselbe Kategorie wie die Änderung an router.routes in 0.137.0 fällt. Alle Fakten wurden am 25. Juli 2026 erneut anhand von PyPI, dem GitHub-API und dem getaggten Quellcode überprüft. 

  28. Starlette-Releases 1.4.0 (5. August 2026), 1.5.0 (8. August 2026) und 1.6.0 (8. August 2026). 1.5.0 trägt den Titel „This release is all about giving GZipMiddleware some love“ und führt „Add exclude_content_types parameter to GZipMiddleware“, „Flush GZip output for each streamed chunk“, „Skip compression of partial responses in GZipMiddleware“ und „Expand default excluded content types in GZipMiddleware“ auf. Das Ausschluss-Tuple und die Signatur wurden direkt aus starlette/middleware/gzip.py gelesen: 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/ – und def __init__(self, app, minimum_size=500, compresslevel=9, thread_minimum_size=128*1024, *, exclude_content_types=DEFAULT_EXCLUDED_CONTENT_TYPES). 1.6.0 ergänzt max_body_size bei Starlette/Router/Mount/Route sowie RequestBodyLimitMiddleware. Alles am 16. August 2026 abgerufen und überprüft. 

  29. FastAPI 0.141.0 (29. Juli 2026, 14:47 UTC) ergänzte app.frontend(check_dir="auto") für die lokale Entwicklung mit fastapi dev (PR #16102). FastAPI 0.141.1 (29. Juli 2026, 17:17 UTC) korrigierte die Unterstützung für Background Tasks und Header aus Abhängigkeiten in app.frontend() (PR #16105) und dokumentierte FASTAPI_ENV im FastAPI-CLI-Leitfaden (PR #16104). Beide von @tiangolo. PyPI bestätigte 0.141.1 am 29. Juli 2026 als neueste Version. 

  30. FastAPI-Releases 0.140.1 bis 0.140.7, alle am 27. Juli 2026 zwischen 12:07 und 17:34 UTC veröffentlicht (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). Jeder Release-Text enthält einen einzelnen Refactoring-Eintrag: 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). Die Cache-Zahl stammt aus dem Diff zu #16062, das drei @lru_cache(maxsize=1024)-Decorators in fastapi/dependencies/models.py durch @lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE) ersetzt und tests/test_dependency_models.py aktualisiert, um cache_info.maxsize == 4096 zu prüfen; im PR-Text steht: „Some users reported a number of dependencies larger than 1024, this should account for larger apps.“ 0.140.2 ergänzt außerdem einen Speicher-Benchmark (PR #16064) und 0.140.7 ergänzt OpenAPI-Abhängigkeits-Benchmarks (PR #16075), sodass die Benchmark-Abdeckung erst nach dem Großteil der Reihe erfolgte. Gegen die GitHub-Release-API, die PR-Diffs und PyPI am 27. Juli 2026 überprüft; 0.140.7 war zum Zeitpunkt der Erstellung die neueste Version. 

NORMAL fastapi-htmx.md EOF