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.
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.
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)
{'locale': 'ar'}
400
The dependency supplies a valid locale or stops the request with 400. The public header is not an authentication credential.
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.