Course outline

FastAPI / Basic: HTTP and typed boundaries / Lesson 6

Dependencies and application composition

Use dependency injection to declare reusable request context and owned resources without hiding security or mutable global state.

About 5 min readContent reviewed
On this page

Lesson goals

  • Explain the assumptions and boundaries of dependencies and application composition.
  • Interpret both complete examples and justify their observed outputs.
  • Diagnose the listed failures and apply the worked solutions while keeping identity, state, and lifecycle ownership explicit.

A dependency is an explicit callable contract

FastAPI dependencies declare values or checks required before a path operation can run. They can receive validated request parameters and depend on other callables. This makes input extraction, identity, configuration, and resource ownership visible in one graph. A dependency is not automatically a singleton, a background task, or a permission system. Keep it small and name its responsibility. The first example extracts a supported locale header and rejects unsupported values before the endpoint. It is presentation context, not authentication, and the example never treats the client’s locale as trusted identity or authority.

python Complete validated dependency check
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.testclient import TestClient
def locale(x_lab_locale: Annotated[str, Header()] = "en"):
    if x_lab_locale not in {"en", "ar"}:
        raise HTTPException(status_code=400, detail="Unsupported locale")
    return x_lab_locale
app = FastAPI()
@app.get("/greeting")
def greeting(value: Annotated[str, Depends(locale)]): return {"locale": value}
with TestClient(app) as client:
    print(client.get("/greeting", headers={"X-Lab-Locale": "ar"}).json())
    print(client.get("/greeting", headers={"X-Lab-Locale": "xx"}).status_code)
Expected output
{'locale': 'ar'}
400

The dependency supplies a valid locale or stops the request with 400. The public header is not an authentication credential.

Compose without duplicating hidden work

A dependency graph can reuse subdependencies, and FastAPI normally caches the same dependency result within one request under its configured semantics. The second example asks for the same counter dependency twice and sees one evaluation per request. That cache does not share safe state across different requests, workers, or processes. A dependency that performs an unexpected write makes repeated or overridden evaluation harder to reason about. Prefer returning a validated value or an owned resource, and make effectful operations explicit in the application flow rather than hiding them inside a helper whose name suggests a simple lookup.

python Complete per-request dependency-cache check
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
calls = {"count": 0}
def value():
    calls["count"] += 1
    return calls["count"]
app = FastAPI()
@app.get("/pair")
def pair(a: Annotated[int, Depends(value)], b: Annotated[int, Depends(value)]):
    return [a, b]
with TestClient(app) as client:
    print(client.get("/pair").json())
    print(client.get("/pair").json())
Expected output
[1, 1]
[2, 2]

Each request evaluates value once despite two consumers. The demonstration counter is shared only to make evaluations visible and is not production request state.

Separate injection from authorization

A dependency can enforce authentication or authorization, but only if its implementation and route attachment actually do so. Extracting a bearer string or reading a user ID header does not verify identity. Resource-level authorization also needs the loaded target and trusted principal, not merely a global login dependency. Keep public request data distinct from server-selected context. Review dependencies attached at app, router, and endpoint scope so a protected route is not accidentally omitted. A graph that looks sophisticated can still expose data if one operation bypasses the intended security check.

Assign lifetime and cleanup deliberately

Some dependencies yield a resource and clean it up afterward; their scope and timing matter, particularly for streaming and background work. Later lessons verify those behaviors under the pinned baseline. Do not keep a database session or request object in a mutable global and assume dependency injection makes it safe. Likewise, a configuration dependency should not silently read a changing real environment in every helper. Inject a stable validated object or controlled provider. Cycles and deeply coupled graphs can make startup and error behavior confusing, so keep ownership and call direction understandable.

Test substitution and request boundaries

Dependency overrides are useful for replacing a repository, clock, identity verifier, or external client with a safe test implementation. Clear overrides after each test and avoid proving only that the fake agrees with itself. Keep integration coverage for the actual HTTP and validation boundary. The current examples use complete isolated apps, synthetic headers, and in-memory counters with no external services or storage. Assert both allowed and rejected dependency inputs and verify repeated requests do not unintentionally share per-request context. Injection improves testability when dependencies expose contracts; it does not remove the need to understand their side effects or lifetime.

Common errors and fixes

A parsed header is treated as a verified user

Why it happens
Input extraction was confused with authentication.
How to fix it
Use a trusted verifier and separate resource authorization.
Prevent it next time
Test forged and absent credentials at the actual boundary.

Request state leaks through a global resource

Why it happens
Dependency injection was assumed to manage arbitrary global lifetime.
How to fix it
Create or scope resources according to an explicit owner.
Prevent it next time
Test multiple requests and teardown independently.

A test override remains active in later tests

Why it happens
Override cleanup was not registered.
How to fix it
Restore the original dependency mapping in finally.
Prevent it next time
Use isolated app fixtures and predictable teardown.

Check your understanding

1. Two parameters depend on the same callable in one request. Should it always run twice?

Show worked solution

No. Under normal dependency caching the result is reused within the request. Explicit cache options and dependency identity matter. This is not a global cache or a guarantee that side effects are safe, so keep effectful work outside hidden dependency evaluation.

2. Where should a resource ownership check obtain the user ID?

Show worked solution

From the trusted authenticated principal selected by the server’s verifier, not an arbitrary request field claiming an owner. Load the actual target resource and apply the policy to that operation. Validation of an ID’s shape alone grants no authority.

Key takeaways

  • FastAPI dependencies declare values or checks required before a path operation can run.
  • A dependency can enforce authentication or authorization, but only if its implementation and route attachment actually do so.
  • Dependency overrides are useful for replacing a repository, clock, identity verifier, or external client with a safe test implementation.

Official references