Course Content
FastAPI Essentials
1 sections · 32 lessons
What is dependency injection in FastAPI?
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).
1from contextlib import asynccontextmanager2from typing import Annotated3from fastapi import Depends, FastAPI, Header, HTTPException, Request45@asynccontextmanager6async def lifespan(app: FastAPI):7 app.state.model = SentimentModel() # loaded once per worker8 yield910app = FastAPI(lifespan=lifespan)1112def get_model(request: Request) -> SentimentModel:13 return request.app.state.model1415def get_db():16 print(" open db session")17 try:18 yield "db-session"19 finally:20 print(" close db session")2122def get_tenant(x_api_key: Annotated[str, Header()], db: Annotated[str, Depends(get_db)]) -> str:23 if x_api_key != "k-123":24 raise HTTPException(401, "Bad API key")25 return "acme"2627@app.post("/sentiment")28def sentiment(text: str,29 model: Annotated[SentimentModel, Depends(get_model)],30 tenant: Annotated[str, Depends(get_tenant)],31 db: Annotated[str, Depends(get_db)]):32 print(" handler runs")33 return {"tenant": tenant, "score": model.predict(text)}Real output for one good request:
open db session handler runs close db session{'tenant': 'acme', 'score': 0.93}Three things to notice:
- Caching.
get_dbis needed twice (byget_tenantand 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
yieldran 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_tenantraised 401, the handler never ran.
Swapping dependencies in tests
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
lifespanand 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 defonly when everything inside is awaited; plaindefruns in the threadpool.