Blogs / FastAPI Dependency Injection: Ein vollständiger Leitfaden

FastAPI Dependency Injection: Ein vollständiger Leitfaden

Veröffentlicht
27. August 2026
Autor
Faizan Nadeem
Tags
FastAPI Python Backend Development Software Architecture
Die Hände einer Person, die zwei Puzzleteile zusammenhalten und gleich verbinden
Foto von Vardan Papikyan auf Unsplash

Depends wird in fast jedem FastAPI-Tutorial als „so bekommt man den aktuellen Benutzer“ eingeführt, und die meisten Entwickler schauen nie über diesen einen Anwendungsfall hinaus. Das ist schade, denn Depends ist ein echtes Dependency-Injection-System — es löst einen Graphen auf, cached Ergebnisse pro Request und verwaltet Setup-/Teardown-Lebenszyklen — und wenn man es nur als Dekorator für Auth-Prüfungen behandelt, verpasst man die Muster, die eine wachsende FastAPI-Codebasis tatsächlich testbar halten: Datenbank-Sessions, die sich korrekt selbst aufräumen, selbst wenn ein Handler eine Exception wirft; Berechtigungsprüfungen, die aus kleineren Prüfungen zusammengesetzt sind, statt pro Route dupliziert zu werden; und request-scope-Werte, die genau einmal berechnet werden, egal wie viele andere Dependencies sie benötigen.

Die Kehrseite ist, dass Depends flexibel genug ist, um damit wirklich schlechte Architektur zu bauen, und die häufigste Art, wie das passiert, ist, eine Endpoint-Funktion so zu behandeln, als wäre sie eine normale Python-Funktion, die man einfach von einem anderen Endpoint aufrufen kann, um ihre Logik „wiederzuverwenden“. Es kompiliert. In einer Demo funktioniert es oft sogar. Es zerstört aber auch das gesamte Modell, um das herum FastAPIs Dependency-System gebaut wurde, und es ist das zentrale Anti-Pattern, das Codebasen, in denen Depends ein Produktivitätsmultiplikator ist, von solchen trennt, in denen es eine Quelle rätselhafter Bugs ist. Dieser Beitrag behandelt beide Seiten: die Muster, die sich bewusst zu verwenden lohnen, und die Anti-Patterns, die man aktiv vermeiden sollte.

Sie lernen:

  • Wie FastAPI einen Depends-Graphen tatsächlich auflöst, in welcher Reihenfolge und wie tief er verschachtelt sein kann
  • Warum yield-basierte Dependencies das richtige Werkzeug für alles sind, was garantiertes Teardown braucht
  • Wie Dependency-Caching pro Request funktioniert und wann es stillschweigend nicht greift
  • Wo gemeinsame Dependencies hingehören, damit jeder Router sie nutzen kann, ohne Logik zu duplizieren
  • Warum das Aufrufen einer Endpoint-Funktion aus einer anderen ein Design-Smell ist und was man stattdessen tun sollte
  • Klassenbasierte Dependencies für parametrisierte, wiederverwendbare Prüfungen

Inhaltsverzeichnis

  1. Die Grundlagen
  2. Wie die Auflösung von Depends tatsächlich funktioniert
  3. Yield-basierte Dependencies und Teardown
  4. Dependency-Caching innerhalb eines Requests
  5. Gemeinsame Dependencies richtig umsetzen
  6. Das Anti-Pattern: Einen Endpoint aus einem anderen aufrufen
  7. Klassenbasierte Dependencies
  8. Testen mit dependency_overrides
  9. Häufige Fallstricke
  10. Best Practices für Production

Die Grundlagen

Was Depends tatsächlich ist

Depends markiert einen Parameter als etwas, das FastAPI für Sie auflösen soll, bevor Ihre Funktion ausgeführt wird — indem eine andere Funktion (oder ein Callable) aufgerufen wird, möglicherweise mit eigenen Depends-Parametern, und das Ergebnis übergeben wird. Es ist Dependency Injection in genau dem Sinn, in dem der Begriff auch in anderen Frameworks verwendet wird: Ihre Funktion deklariert, was sie benötigt, und etwas anderes ist dafür verantwortlich, es zu konstruieren.

from fastapi import Depends

def get_query_token(token: str) -> str:
    return token

@app.get("/items")
async def read_items(token: str = Depends(get_query_token)):
    return {"token": token}

Das wirkt wie eine kleine Bequemlichkeit für eine einzige Dependency, aber es lässt sich zusammensetzen: get_query_token könnte selbst von etwas anderem abhängen, und FastAPI löst die gesamte Kette der Reihe nach auf, bevor Ihr Route-Handler überhaupt ausgeführt wird.

Warum das über Auth hinaus wichtig ist

Das erste Beispiel in jedem Tutorial ist get_current_user, wodurch Depends so wirkt, als existiere es ausschließlich für Authentifizierung. In der Praxis ist derselbe Mechanismus das richtige Werkzeug für Datenbank-Sessions, Paginierungsparameter, Feature-Flag-Prüfungen, request-scoped Logger und Rate Limiting — alles, was „Setup-Arbeit, die ein Handler braucht, konsistent berechnet wird und möglicherweise danach aufgeräumt werden muss“ ist, ist ein Kandidat für Depends, nicht nur eine Auth-Prüfung.

Wie die Auflösung von Depends tatsächlich funktioniert

FastAPI baut pro Request einen Dependency-Graphen auf, indem es jeden Depends-Parameter rekursiv durchläuft und zuerst die Blätter auflöst. Gegeben:

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def get_current_user(token: str = Depends(get_query_token), db=Depends(get_db)):
    return db.query(User).filter_by(token=token).first()

@app.get("/profile")
async def profile(user=Depends(get_current_user)):
    return user

Ein Aufruf von /profile löst zuerst get_query_token und get_db auf (sie haben keine weiteren Dependencies), ruft dann get_current_user mit beiden Ergebnissen auf und schließlich den Route-Handler mit dem vollständig aufgelösten user. Die Verschachtelung kann beliebig tief gehen — das ist ein echter gerichteter azyklischer Graph, keine flache Liste — und FastAPI löst jeden Knoten pro Request genau einmal auf, selbst wenn mehrere andere Dependencies ihn benötigen; das ist das Caching-Verhalten, das weiter unten behandelt wird.

Sync- und Async-Dependencies lassen sich frei mischen

Eine Dependency kann def oder async def sein, unabhängig davon, was der Route-Handler ist, der sie verwendet. FastAPI löst async def-Dependencies direkt auf dem Event Loop auf und schickt def-Dependencies in den Threadpool — genau dieselbe Trennung, die in Why Your FastAPI Endpoint Blocks the Event Loop für Route-Handler selbst beschrieben wird. Ein häufiges, korrektes Muster ist ein async def-Route-Handler mit einer def-Dependency, die etwas wirklich Synchrones tut — etwa einen leichtgewichtigen Config-Lookup — und FastAPI übernimmt das Dispatching in den Thread für diese eine Dependency automatisch, ohne dass Sie Signaturen über die gesamte Kette hinweg angleichen müssen. Das Einzige, worauf man achten muss, ist dieselbe Blocking-Call-Falle: Eine def-Dependency ist sicher, weil sie in den Threadpool geht, aber eine async def-Dependency, die etwas Blockierendes aufruft, ohne darauf zu warten, blockiert den Loop genauso wie ein Route-Handler, da für Dependencies dieselben Ausführungsregeln gelten wie für die Handler, in die sie eingespeist werden.

Dependencies, die direkt auf einem APIRouter deklariert werden (via dependencies=[Depends(...)]) oder auf der App selbst, laufen für jede Route darunter, ohne überhaupt als Funktionsparameter zu erscheinen — nützlich für querschnittliche Prüfungen wie „dieser gesamte Router erfordert ein aktives Abonnement“, bei denen der Rückgabewert der Dependency vom Handler gar nicht benötigt wird.

Yield-basierte Dependencies und Teardown

Eine Dependency, die Cleanup braucht — das Schließen einer Datenbank-Session, das Freigeben eines Locks, das Committen oder Zurückrollen einer Transaktion — verwendet yield statt return. Code nach dem yield läuft, nachdem die Response erzeugt wurde, und entscheidend ist: Er läuft selbst dann, wenn der Route-Handler eine Exception geworfen hat:

async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

Das ist der Mechanismus, der „eine Session pro Request“ zuverlässig korrekt macht — das Teardown ist nichts, woran jeder Route-Autor denken muss, sondern wird von der Dependency selbst garantiert, unabhängig davon, wie der Handler endet. Das Muster verallgemeinert sich auch auf Sub-Dependencies: Wenn get_db selbst eine Dependency von get_current_user ist, läuft sein Teardown trotzdem erst, nachdem alles, was davon abhing, abgeschlossen ist, in umgekehrter Auflösungsreihenfolge — eher wie ein try/finally-Stack als wie eine flache Liste unabhängiger Cleanups.

Genau dieses Muster ist die Grundlage für den Session-Scoping-Ansatz, der ausführlich in Async SQLAlchemy Sessions in FastAPI, Done Right behandelt wird — wenn man das yield-basierte Teardown hier richtig umsetzt, treten die meisten Session-Lifecycle-Probleme aus diesem Beitrag gar nicht erst auf.

Dependency-Caching innerhalb eines Requests

Standardmäßig cached FastAPI das Ergebnis einer Dependency für die Lebensdauer eines einzelnen Requests — wenn zwei verschiedene Dependencies beide Depends(get_db) deklarieren, ruft FastAPI get_db genau einmal auf und gibt beiden Aufrufern dasselbe Session-Objekt, nicht zwei getrennte:

async def get_db():
    print("get_db called")
    ...

def dep_a(db=Depends(get_db)): ...
def dep_b(db=Depends(get_db)): ...

@app.get("/example")
async def example(a=Depends(dep_a), b=Depends(dep_b)):
    # "get_db called" prints exactly once, not twice
    ...

Das ist gerade für Datenbank-Sessions enorm wichtig — ohne dieses Caching würden dep_a und dep_b stillschweigend mit zwei verschiedenen Sessions innerhalb desselben Requests arbeiten, was eine subtile Quelle für Bugs der Art „warum ist mein Write nicht sichtbar“ ist, wenn eine Dependency schreibt und eine andere innerhalb desselben logischen Requests liest.

Das Caching ist auf den Request begrenzt, nicht global, und kann pro Dependency mit Depends(get_db, use_cache=False) deaktiviert werden für den seltenen Fall, dass man selbst innerhalb eines Requests wirklich eine frische Instanz möchte. Es ist auch wichtig zu wissen, dass der Cache-Key das Callable selbst ist — zwei Aufrufe von Depends(get_db) teilen sich einen Cache-Eintrag, weil sie auf dasselbe Funktionsobjekt verweisen, aber zwei verschiedene Funktionen, die intern zufällig dasselbe tun, teilen sich keinen Cache-Eintrag, selbst wenn ihre Logik identisch ist.

Gemeinsame Dependencies richtig umsetzen

Der natürliche Ort für Dependencies, die über mehrere Domänen hinweg verwendet werden, ist ein gemeinsames core/dependencies.py, wie in FastAPI Project Structure That Survives Growth beschrieben — get_db, get_current_user und jede querschnittliche Berechtigungsprüfung gehören dorthin, werden nach unten in jeden Domänen-Router importiert, niemals pro Router dupliziert und niemals seitlich zwischen Domänen importiert.

# app/core/dependencies.py
from fastapi import Depends, HTTPException, status

async def get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_db)):
    user = await db.get_user_by_token(token)
    if user is None:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED)
    return user

async def require_admin(user=Depends(get_current_user)):
    if not user.is_admin:
        raise HTTPException(status.HTTP_403_FORBIDDEN)
    return user

Beachten Sie, dass require_admin von get_current_user abhängt, statt die Token-Prüfung neu zu implementieren — das ist Komposition, und genau darin liegt der eigentliche Gewinn eines echten Dependency-Graphen: Berechtigungslogik baut auf Identitätslogik auf, statt sie zu duplizieren, und dank des Caching pro Request wird bei der Verwendung von require_admin und get_current_user in derselben Route der Benutzer trotzdem nur einmal aufgelöst.

Das Anti-Pattern: Einen Endpoint aus einem anderen aufrufen

Eine verlockende Abkürzung, wenn ein neuer Endpoint „im Grunde das tut, was ein anderer Endpoint bereits tut“, ist, diese Endpoint-Funktion direkt zu importieren und aufzurufen:

# Anti-pattern — do not do this
@app.get("/orders/{id}")
async def get_order(id: int, user=Depends(get_current_user)):
    return await fetch_order(id, user)

@app.get("/orders/{id}/summary")
async def get_order_summary(id: int, user=Depends(get_current_user)):
    order = await get_order(id, user)  # calling another route handler directly
    return summarize(order)

Das wirkt harmlos — in vielen Fällen läuft es sogar korrekt — aber es bricht das Modell auf mehrere konkrete Arten. Depends-Parameter der aufgerufenen Funktion werden nicht erneut über FastAPIs Graphen aufgelöst; Sie rufen einfach eine Python-Funktion auf und reichen ihre Argumente manuell durch, wodurch jegliches Dependency-Caching, Error Handling oder yield-basiertes Teardown, das FastAPI normalerweise koordinieren würde, für diesen inneren Aufruf vollständig umgangen wird. Wenn get_order eine HTTPException wirft, propagiert sie als rohe Exception in den Body von get_order_summary, statt in eine Response umgewandelt zu werden, wie es der Fall wäre, wenn der Client tatsächlich /orders/{id} aufgerufen hätte — das bedeutet, dass die beiden Codepfade denselben Fehler unterschiedlich behandeln, je nachdem, über welche Route man kommt. Und es koppelt stillschweigend die HTTP-Layer-Signaturen zweier Endpoints aneinander, sodass eine Änderung an den Parametern von get_order für dessen eigene Route jetzt riskieren kann, get_order_summary zu zerstören, obwohl das mit Routing an sich nichts zu tun hat.

Die Lösung ist genau die Schichtung, die Depends eigentlich unterstützen soll: Extrahieren Sie die gemeinsame Logik in eine Service-Funktion, die überhaupt kein Route-Handler ist, und lassen Sie beide Routen davon abhängen oder diese aufrufen.

# app/orders/service.py
async def fetch_order(order_id: int, user) -> Order:
    order = await db.get_order(order_id)
    if order.owner_id != user.id:
        raise HTTPException(status.HTTP_403_FORBIDDEN)
    return order

# app/orders/router.py
@router.get("/{id}")
async def get_order(id: int, user=Depends(get_current_user)):
    return await fetch_order(id, user)

@router.get("/{id}/summary")
async def get_order_summary(id: int, user=Depends(get_current_user)):
    order = await fetch_order(id, user)
    return summarize(order)

Jetzt teilen beide Routen echte Logik, ohne dass eine von ihnen vom HTTP-Layer-Vertrag der anderen abhängt, und fetch_order lässt sich trivial als Unit testen, ohne überhaupt einen Request hochzufahren.

Klassenbasierte Dependencies

Für eine Dependency, die Konfiguration braucht — ein Paginierungslimit, einen erforderlichen Permission-Scope — bietet eine Klasse mit __call__ eine parametrisierte, wiederverwendbare Dependency statt einer Familie nahezu identischer Funktionen:

class RequirePermission:
    def __init__(self, scope: str):
        self.scope = scope

    def __call__(self, user=Depends(get_current_user)):
        if self.scope not in user.scopes:
            raise HTTPException(status.HTTP_403_FORBIDDEN)
        return user

require_orders_write = RequirePermission("orders:write")

@router.post("/orders", dependencies=[Depends(require_orders_write)])
async def create_order(payload: OrderCreate):
    ...

Das skaliert deutlich besser, als separate Funktionen wie require_orders_write_permission, require_payments_read_permission und so weiter zu schreiben — die Parametrisierung lebt im Konstruktor, und die FastAPI-seitige Form bleibt über jede Berechtigungsprüfung in der App hinweg ein einziges, konsistentes Callable-Muster.

Klassenbasierte Dependencies können __call__ auch als Generator verwenden und so Parametrisierung mit yield-basiertem Teardown kombinieren — nützlich zum Beispiel für einen Rate Limiter, der nach Abschluss des Requests ein Token freigeben muss:

class RateLimiter:
    def __init__(self, requests_per_minute: int):
        self.limit = requests_per_minute

    async def __call__(self, user=Depends(get_current_user)):
        token = await acquire_slot(user.id, self.limit)
        try:
            yield
        finally:
            await release_slot(token)

throttle_reports = RateLimiter(requests_per_minute=5)

@router.get("/reports", dependencies=[Depends(throttle_reports)])
async def get_reports():
    ...

Jede Instanz wird genau einmal erzeugt, zur Importzeit, und dann über alle Requests hinweg wiederverwendet — die Konstruktorargumente konfigurieren die Dependency, während __call__ trotzdem das vollständige Verhalten pro Request für Auflösung, Caching und Teardown jeder anderen Dependency bekommt.

Testen mit dependency_overrides

Der andere große Gewinn beim Aufbau echter Dependencies statt dem Inline-Schreiben von Logik in Handlern ist app.dependency_overrides — ein Dict, das FastAPI vor dem Auflösen jeder Dependency prüft und es Tests erlaubt, get_db gegen eine Testdatenbank oder get_current_user gegen einen festen Testbenutzer auszutauschen, ohne den Route-Code überhaupt anzufassen:

# tests/conftest.py
from app.main import app
from app.core.dependencies import get_db, get_current_user

async def override_get_db():
    async with TestSessionLocal() as session:
        yield session

def override_get_current_user():
    return User(id=1, email="test@example.com", is_admin=False)

app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user
# tests/orders/test_router.py
async def test_create_order(client):
    response = await client.post("/orders", json={"item": "widget", "qty": 2})
    assert response.status_code == 200

Keine Mocking-Library, kein Monkeypatching interner Details, kein Hochfahren einer echten Postgres-Instanz oder eines echten OAuth-Flows nur um einen Endpoint zu treffen, der zufällig Login erfordert. Genau deshalb ist das Anti-Pattern weiter oben in diesem Beitrag über Code-Sauberkeit hinaus wichtig: Eine Route, die direkt eine andere Route-Funktion aufruft, kann auf diese Weise nicht getestet werden, weil der innere Aufruf nie durch FastAPIs Auflösungsmechanismus geht — dependency_overrides hat nichts, das es abfangen könnte. Eine Route hingegen, die über Depends von einer echten Service-Funktion abhängt, ist vollständig isoliert testbar, wobei jede Dependency in ihrem Graphen unabhängig austauschbar ist.

Overrides komponieren sich außerdem genauso wie Dependencies selbst — das Überschreiben von get_db überschreibt es für jede Dependency, die transitiv davon abhängt, einschließlich get_current_user, ohne dass ein separates Override für jede einzelne nötig wäre. Denken Sie daran, Overrides zwischen Testmodulen zurückzusetzen (app.dependency_overrides.clear() im Teardown einer Fixture) — ein übrig gebliebenes Override aus einer Testdatei, das im nächsten Test stillschweigend das Verhalten verändert, ist eine häufige Ursache für verwirrende, reihenfolgeabhängige Testfehlschläge.

Häufige Fallstricke

Fehler: Business-Logik direkt in eine Dependency statt in einen Service packen. Eine Depends-Funktion, die ein vollständig verarbeitetes Business-Objekt abfragt, transformiert und zurückgibt, macht diese Logik für alles unsichtbar, was keine FastAPI-Route ist. Lösung: Halten Sie Dependencies auf request-scoped Setup fokussiert (Auth, Sessions, Paginierung) und delegieren Sie echte Business-Logik an Service-Funktionen.

Fehler: anzunehmen, dass sich jeder Depends(x)-Aufruf einen Cache-Eintrag teilt, unabhängig davon, wie auf x verwiesen wird. Zwei Funktionen, die dasselbe tun, aber nicht dasselbe Objekt sind, teilen sich keinen Cache-Slot. Lösung: Importieren Sie überall exakt dasselbe Callable, wo Caching greifen soll — definieren Sie nicht an mehreren Stellen gleich aussehende Dependencies neu.

Fehler: zu vergessen, dass yield-basiertes Teardown erst läuft, wenn die Response vollständig erzeugt wurde. Code nach yield läuft, nachdem der Handler zurückkehrt, was bedeutet, dass er auch nach jeder anderen Dependency weiter oben in der Kette läuft, die von dieser hier abhängt. Lösung: Gehen Sie nicht von der Reihenfolge des Teardowns aus, ohne den tatsächlichen Dependency-Graphen nachzuverfolgen, besonders bei verschachtelten Dependencies.

Fehler: eine Route-Handler-Funktion direkt aus einer anderen Route aufzurufen. Wie oben beschrieben, umgeht das Caching, Exception Handling und Teardown-Garantien. Lösung: Extrahieren Sie gemeinsame Logik in eine einfache Service-Funktion, die keiner Route „gehört“.

Fehler: router-level dependencies=[...] für Dinge zu stark zu nutzen, deren Rückgabewert der Handler tatsächlich braucht. Wenn der Handler das aufgelöste Benutzerobjekt benötigt, erzwingt das Verstecken in Router-Level-Dependencies einen zweiten, redundanten Depends(get_current_user)-Parameter, nur um den Wert wiederzubekommen. Lösung: Verwenden Sie Router-Level-Dependencies nur für Prüfungen, deren Rückgabewert niemand braucht.

Best Practices für Production

  • Halten Sie Dependencies request-scoped und arm an Side Effects. Setup, Auth-Prüfungen und Session-Management gehören hierhin; mehrstufige Business-Logik nicht.
  • Setzen Sie Berechtigungsprüfungen zusammen, statt sie zu duplizieren. require_admin, das von get_current_user abhängt, ist günstiger und konsistenter, als die Token-Prüfung pro Berechtigungsstufe neu zu implementieren.
  • Kombinieren Sie yield immer mit einem try/finally, damit das Teardown auch läuft, wenn der Handler eine Exception wirft — verlassen Sie sich nicht nur auf den Happy Path.
  • Rufen Sie niemals einen Route-Handler aus einem anderen auf. Wenn zwei Endpoints dieselbe Logik brauchen, gehört diese Logik in eine Service-Funktion, nicht in einen der beiden Handler.
  • Verwenden Sie klassenbasierte Dependencies, sobald Sie mehr als zwei oder drei parametrisierte Varianten derselben Prüfung haben. So bleibt die Berechtigungsoberfläche konsistent und leicht auditierbar.

Fazit

Depends ist ein echter Dependency-Graph mit Caching-, Reihenfolge- und Lifecycle-Garantien — kein Dekorator, der zufällig den aktuellen Benutzer holt. Bewusst eingesetzt sorgt es dafür, dass Auth, Sessions und querschnittliche Prüfungen über eine wachsende Menge von Routern hinweg konsistent bleiben, ohne Logik pro Endpoint zu duplizieren. Unachtsam verwendet — insbesondere wenn man einen Route-Handler als einfache Funktion behandelt, die man von anderswo aus aufruft — bricht es still genau die Garantien, die seinen Einsatz überhaupt erst lohnenswert gemacht haben.

Erledigen Ihre Dependencies request-scoped Setup, oder sind einige von ihnen stillschweigend zu dem Ort geworden, an dem Ihre Business-Logik lebt? Wenn Sie sich nicht sicher sind, ist dependency_overrides ein schneller Weg, das herauszufinden — eine Dependency, die sich in einem Test nicht sauber austauschen lässt, ist meist eine, die Verantwortlichkeiten übernommen hat, die sie nie haben sollte.

Weitere Artikel