


KI FastAPI-Dienste schreiben lassen und typische Fehler im Review finden
Wenn eine KI einen FastAPI-Dienst baut oder prüft. Legt Mindestversionen fest, wann async- oder sync-Routen passen, wie Pydantic-Modelle und Abhängigkeiten geschrieben werden, wie mit Datenbank und Tests gearbeitet wird, und liefert eine Tabelle typischer Fehler im Diff mit Lösung.
Diese KI-Regeln verwenden
## Mindestversionen für Python, FastAPI und Bibliotheken festlegen
Binde jede Abhängigkeit an die unten angegebene Version oder neuer; alle folgenden Empfehlungen setzen diese Basiswerte voraus. Eine ältere Version bricht die in späteren Abschnitten beschriebenen Idiome stillschweigend — asynchrone APIs, `Annotated`-Abhängigkeiten und die v2-Serialisierung.
| Abhängigkeit | Minimum | Warum diese Untergrenze |
|---|---|---|
| Python | 3.11 | `StrEnum` und der Union-Operator `X \| Y` |
| FastAPI | 0.115 | `Annotated[T, Depends(...)]` ist die idiomatische Form |
| Pydantic | 2.7 | v1-APIs (`json_encoders`, `.dict()`) wurden entfernt |
| pydantic-settings | 2.4 | Seit Pydantic v2 ein eigenständiges Paket |
| SQLAlchemy | 2.0 | Die asynchrone API (`AsyncSession`, `async_sessionmaker`) |
| Alembic | 1.13 | Async-fähige Migrationen |
| httpx | 0.27 | `ASGITransport` für In-Process-Tests |
| PyJWT | 2.9 | Aktiv gepflegt; ersetzt `python-jose` |
| ruff | 0.6 | Ein Werkzeug für Linting, Formatierung und Import-Sortierung |
## Das Projekt nach Domänen statt nach Dateitypen in Ordner teilen
Strukturiere den Verzeichnisbaum nach Domäne (Bounded Context), nicht nach Dateityp. Gib jeder Domäne ein eigenes Paket und halte eine schlanke globale Schicht für gemeinsame Belange.
- Paket pro Domäne: `router.py` (Endpunkte), `schemas.py` (Pydantic-Modelle), `models.py` (ORM-Modelle), `service.py` (Geschäftslogik), `dependencies.py`, `config.py` (domänenspezifische Einstellungen), `constants.py`, `exceptions.py`, `utils.py`.
- Globale oberste Ebene: `config.py` (globale Einstellungen), `models.py` (gemeinsame Basisklassen), `exceptions.py`, `database.py` (asynchrone Engine plus Session-Factory), `main.py` (App plus Lifespan).
- Domänenübergreifende Importe: importiere immer das explizite Modul und versieh es mit einem Alias (importiere z. B. das `service`-Modul einer Domäne als `auth_service`). Verwende niemals einen Wildcard-Import und greife niemals über einen tiefen internen Pfad zu — beides koppelt Domänen aneinander und blockiert Refactoring.
## async def oder def für Routen wählen, damit die App nicht hängt
Wähle den Routen-Stil danach, welche Arbeit sie verrichtet; die falsche Wahl blockiert entweder die Event-Loop oder verschwendet Threads.
| Die Route tut Folgendes | Verwende |
|---|---|
| Awaitbare, nicht blockierende I/O | `async def` |
| Blockierende I/O, kein asynchroner Client existiert | `def` (synchron, läuft im Threadpool) |
| Eine Mischung aus beidem | `async def` plus `run_in_threadpool` für den blockierenden Aufruf |
| CPU-lastige Arbeit über ~50 ms | Auslagern an einen Worker-Prozess (Celery / Arq / RQ) |
Platziere niemals einen blockierenden Aufruf — `time.sleep`, `requests`, einen synchronen DB-Treiber oder ein einfaches `open()` — innerhalb von `async def`; das friert jede Anfrage auf diesem Worker ein. Eine synchrone `def`-Route blockiert nur einen Threadpool-Worker und ist damit der sichere Ort für unvermeidbare blockierende Aufrufe. Verwende innerhalb von `async def` awaitbare Entsprechungen (`asyncio.sleep`, `aiofiles`, einen asynchronen Treiber) oder umhülle eine synchrone Bibliothek mit `run_in_threadpool`. Vorbehalte: Der Standard-Threadpool hält etwa 40 Threads vor, sodass eine Sättigung jede synchrone Route verlangsamt, und Threads kosten mehr als Coroutinen — wähle synchrone Routen nicht ohne blockierenden Grund.
## Eingaben mit Pydantic prüfen und Einstellungen nach Domäne trennen
- Bevorzuge eingebaute Validatoren und Typen gegenüber selbstgebauten Prüfungen: `Field(min_length=, max_length=, pattern=, ge=)`, `EmailStr`, `AnyUrl` und `StrEnum` für geschlossene Wertemengen.
- Kombiniere keine Einschränkung mit einem widersprüchlichen Default. `Field(ge=18, default=None)` ist ein Bug: entscheide dich für Pflichtfeld (`Field(ge=18)`) oder optional (`int | None = Field(default=None, ge=18)`).
- Verwende moderne Serialisierung. `json_encoders` ist in Pydantic v2 veraltet — ersetze es durch `@field_serializer` für feldweise Regeln oder annotiere den Typ mit `PlainSerializer`.
- Teile die Einstellungen nach Domäne auf. `pydantic-settings` ist ein eigenständiges Paket; gib jeder Domäne eine eigene `BaseSettings`-Unterklasse mit eigenem `env_prefix`, statt eines globalen Einstellungsobjekts, das von jeder Domäne ausgelesen wird.
## Abhängigkeiten mit Annotated schreiben und Daten prüfen lassen
- Injiziere mit `Annotated[T, Depends(fn)]`, der idiomatischen Form seit FastAPI 0.95. Vermeide die Standardargument-Form (`param: T = Depends(fn)`); sie ist veraltet und birgt Default-Wert-Fallstricke.
- Lass Abhängigkeiten validieren, nicht nur injizieren. Eine Abhängigkeit kann eine Ressource anhand ihrer ID laden, bei fehlender Ressource einen domänenspezifischen 404-Fehler auslösen und das validierte Objekt zurückgeben, sodass die Route bereits geprüfte Daten erhält.
- Verkette Abhängigkeiten, um Logik wiederzuverwenden. Eine Abhängigkeit kann von anderen abhängen — zum Beispiel eine Besitzprüfung, die von einer Load-by-ID-Abhängigkeit und einer geparsten-Token-Abhängigkeit abhängt.
- Regeln: Abhängigkeiten werden pro Anfrage zwischengespeichert, sodass dasselbe `Depends(x)`, mehrfach in einer Anfrage verwendet, nur einmal ausgeführt wird. Bevorzuge `async def`-Abhängigkeiten; synchrone laufen im Threadpool — vergeudeter Overhead für kleine, rein CPU-basierte Prüfungen. Verwende eine Abhängigkeit über mehrere Endpunkte hinweg wieder, indem du der gemeinsam genutzten Pfadvariable denselben Namen gibst.
## JWT-Authentifizierung und asynchronen Datenbankzugriff umsetzen
- JWT: verwende `PyJWT` (`import jwt`), nicht das unbetreute `python-jose`. Decodiere innerhalb einer Prüfung auf `InvalidTokenError` und löse einen domänenspezifischen Anmeldedaten-Fehler aus, statt die rohe Exception entkommen zu lassen.
- Sessions: bevorzuge die asynchrone API von SQLAlchemy 2.0 (`create_async_engine`, `async_sessionmaker`, `AsyncSession`) und reiche Sessions über eine `get_db`-Async-Generator-Abhängigkeit an die Routen weiter. `encode/databases` befindet sich im Wartungsmodus — wähle es nicht für neue Projekte.
- Benennung: verwende `lower_case_snake`; Tabellennamen im Singular (`post`, `user`, `post_like`); gruppiere zusammengehörige Tabellen mit einem gemeinsamen Präfix (`payment_account`, `payment_bill`); Suffix `_at` für Datums-/Zeitwerte und `_date` für reine Datumswerte; und verwende denselben Fremdschlüssel-Spaltennamen überall dort, wo er vorkommt.
- Lege eine projektweite Namenskonvention für Indizes und Constraints auf der `MetaData` fest, damit generierte Namen vorhersehbar bleiben.
- SQL zuerst, Pydantic danach: erledige Joins, Aggregation und JSON-Formung in SQL und hydriere erst für die Antwortvalidierung nach Pydantic — nicht zur Transformation.
## Zwischen Hintergrundaufgaben und einer Warteschlange wie Celery wählen
Wähle das Werkzeug passend zu den Kosten- und Zuverlässigkeitsanforderungen der Aufgabe.
| Verwende In-Process-Hintergrundaufgaben, wenn | Verwende eine echte Warteschlange (Celery / Arq / RQ), wenn |
|---|---|
| Die Aufgabe in unter einer Sekunde abgeschlossen ist | Sie Sekunden bis Minuten läuft |
| Ein stillschweigender Verlust akzeptabel ist | Du Retries, Dead-Letter oder Sichtbarkeit brauchst |
| Sie In-Process abläuft (eine E-Mail senden, eine Zeile loggen) | Sie CPU-lastig ist oder einen eigenen Pool braucht |
| Keine Zeitplanung nötig ist | Du Cron, ETA oder Rate-Limiting brauchst |
In-Process-Hintergrundaufgaben laufen, nachdem die Antwort gesendet wurde, im selben Worker-Prozess. Stirbt dieser Worker, geht die Aufgabe verloren, und es gibt keinen erneuten Versuch — verwende sie deshalb niemals für etwas, weswegen du einen Bereitschaftsingenieur alarmieren würdest.
## Mit httpx und einer echten Datenbank testen
- Verwende von Anfang an einen asynchronen HTTP-Client: steuere die App in-process mit `httpx.AsyncClient` über `ASGITransport(app=app)`. Verwende nicht `async_asgi_testclient` — es wird nicht mehr gepflegt.
- Überschreibe Abhängigkeiten mit dem eingebauten `app.dependency_overrides[dep] = fake` des Frameworks, statt interne Teile per Monkeypatching zu verändern, und setze die Overrides nach jedem Test zurück.
- Verwende für Integrationstests eine echte Datenbank (ein kurzlebiges Schema oder einen Wegwerf-Container) und überschreibe nur Auth und externe Dienste. Das Mocken der Datenbank verdeckt Abweichungen zwischen Mock und Produktion, die irgendwann in der Produktion zuschlagen.
## Migrationen umkehrbar halten und jeden API-Endpunkt dokumentieren
- Migrationen: halte jede Migration statisch und umkehrbar, generiere sie aus der asynchronen Vorlage und verwende beschreibende, zeitgestempelte Dateinamen (ein Datumspräfix plus ein Slug), damit sich die Historie in Reihenfolge lesen lässt.
- API-Dokumentation: verstecke die interaktive Dokumentation außerhalb ausgewählter Umgebungen, indem du die OpenAPI-URL auf `None` setzt, wenn die aktuelle Umgebung nicht in einer Allowlist steht. Dokumentiere jeden Endpunkt vollständig — Response-Modell, Statuscode, Zusammenfassung, Beschreibung, Tags und einen Response-Eintrag pro Fehlerstatus.
- Linting: führe Formatter und Linter aus (zum Beispiel `ruff check --fix` gefolgt von `ruff format`) und binde sie in einen Pre-Commit-Hook oder die CI ein. Ein modernes Werkzeug ersetzt die separaten Tools für Formatierung, Import-Sortierung und toten Code.
## Typische KI-Fehler in einem FastAPI-Diff finden und beheben
Markiere bei der Prüfung eines Diffs diese Agenten-Fehler; jede Zeile ist ein realer Fehlermodus mit der zugehörigen Lösung.
| Anti-Muster | Warum es falsch ist | Lösung |
|---|---|---|
| `requests.get(...)` innerhalb von `async def` | Blockiert die Event-Loop | `httpx.AsyncClient` oder `await run_in_threadpool(...)` |
| `time.sleep` / `open()` / synchroner DB-Treiber in `async def` | Blockiert die Loop | Asynchrones Äquivalent (`asyncio.sleep`, `aiofiles`, asynchroner Treiber) |
| `from jose import jwt` | Unbetreute Bibliothek | `import jwt` (PyJWT) |
| `async_asgi_testclient` | Nicht mehr gepflegt | `httpx.AsyncClient` plus `ASGITransport` |
| `ConfigDict(json_encoders=...)` | In Pydantic v2 veraltet | `@field_serializer` oder `PlainSerializer` |
| `Field(ge=18, default=None)` | Einschränkung widerspricht dem Default | Entweder Pflichtfeld oder optional wählen, nicht beides |
| Standardargument-`Depends(...)` | Veraltete Default-Wert-Fallstricke | `Annotated[T, Depends(...)]` |
| Nacktes Abfangen von `Exception` um einen Routen-Body herum | Verdeckt Bugs, verwandelt 500er in stille 200er | Die spezifische Klasse abfangen, expliziten HTTP-Fehler auslösen |
| Hintergrundaufgaben für alarmpflichtige Arbeit | Kein Retry, stirbt mit dem Worker | Celery / Arq / RQ |
| Synchrone ORM-Session innerhalb von `async def` | Blockiert die Loop, kann den Pool in ein Deadlock bringen | `AsyncSession` |
| Ein Modell zurückgeben und gleichzeitig `response_model` darauf setzen | Das Modell wird zweimal gebaut | Ein Dict oder eine Row zurückgeben oder `response_model` weglassen |
| Tiefe domänenübergreifende Importe | Enge Kopplung | Das Domänenmodul importieren und mit einem Alias versehen |
| Eine einzige `BaseSettings` für die gesamte App | Jede Domäne liest jede Variable | Eine `BaseSettings` pro Domäne |
| Das Mocken der Datenbank in Integrationstests | Abweichung zwischen Mock und Produktion | Eine echte DB plus Dependency-Overrides für Auth und externe Dienste |
