FastAPI Essentials

Course Content

FastAPI Essentials

1 sections · 32 lessons

What is dependency injection in FastAPI?


The dependency tree behind one requestPOST /sentimentget_modelget_tenantx-api-key headerget_db (runs once)
get_db is asked for twice but opened once per request, and any node can be swapped in tests with dependency_overrides.

What you need to know

Here is a sentiment service with three dependencies: the model (loaded once), a database session (opened and closed per request) and the tenant (checked from an API key, and itself depending on the database).

Python
from contextlib import asynccontextmanagerfrom typing import Annotatedfrom fastapi import Depends, FastAPI, Header, HTTPException, Request@asynccontextmanagerasync def lifespan(app: FastAPI):    app.state.model = SentimentModel()          # loaded once per worker    yieldapp = FastAPI(lifespan=lifespan)def get_model(request: Request) -> SentimentModel:    return request.app.state.modeldef get_db():    print("  open db session")    try:        yield "db-session"    finally:        print("  close db session")def get_tenant(x_api_key: Annotated[str, Header()], db: Annotated[str, Depends(get_db)]) -> str:    if x_api_key != "k-123":        raise HTTPException(401, "Bad API key")    return "acme"@app.post("/sentiment")def sentiment(text: str,              model: Annotated[SentimentModel, Depends(get_model)],              tenant: Annotated[str, Depends(get_tenant)],              db: Annotated[str, Depends(get_db)]):    print("  handler runs")    return {"tenant": tenant, "score": model.predict(text)}

Real output for one good request:

Text
  open db session  handler runs  close db session{'tenant': 'acme', 'score': 0.93}

Three things to notice:

  • Caching. get_db is needed twice (by get_tenant and by the route), but the session opened only once. Within one request, FastAPI reuses a dependency's result. Depends(f, use_cache=False) turns this off.
  • Cleanup. The code after yield ran after the handler, even for the request that failed with 401. By default it runs after the response is sent; Depends(get_db, scope="function") ends it before the response goes out.
  • Short-circuit. When get_tenant raised 401, the handler never ran.

Swapping dependencies in tests

Python
app.dependency_overrides[get_model] = lambda: FakeModel()app.dependency_overrides[get_tenant] = lambda: "test-tenant"

After these two lines the same request returns {'tenant': 'test-tenant', 'score': 0.0}, with no GPU, no API key and no change to the route code.

Where dependencies can be attached

  • On one parameter, as above.
  • On a route without using the value: @app.post(..., dependencies=[Depends(rate_limit)]).
  • On a whole router or app: APIRouter(dependencies=[Depends(verify_api_key)]).

A real-life example

A document-question-answering service has 14 routes. Before refactoring, each route opened its own database connection, read the API key header by hand, and loaded a reranker model from disk on the first call. A connection leak appeared whenever an exception was raised between "open" and "close".

After the refactor, get_db (with yield and finally) is the only place that opens sessions, so the leak is gone. verify_api_key is attached once to the router. The reranker loads in lifespan and is reached through get_reranker. The test suite overrides three dependencies and runs 120 tests in 4 seconds without any model or database.

Follow-up questions to expect

  • "Can a dependency be a class?" — Yes. Any callable works; a class is called and its instance is injected. A class with __call__ lets you configure a dependency, such as a rate limiter with a limit value.
  • "Should the model be loaded inside a dependency?" — No. Load it once in lifespan and let the dependency just return it. Loading per request would take seconds and use huge amounts of memory.
  • "Sync or async dependencies?" — Both work. The same rule as routes applies: async def only when everything inside is awaited; plain def runs in the threadpool.