Summary
This postmortem analyzes a subtle FastAPI dependency‑injection pitfall: attaching dependencies at the router level vs. attaching them at the function level. The issue appears trivial, yet it frequently leads to unexpected behavior, hidden performance costs, and inconsistent API semantics.
Root Cause
The root cause is the difference in execution scope between router‑level dependencies and function‑level dependencies:
- Router‑level dependencies run for every request on that router, even if the endpoint does not need them.
- Function‑level dependencies run only when that specific endpoint is invoked.
- Developers often assume both approaches are equivalent, but they are not.
Why This Happens in Real Systems
Real systems accumulate complexity over time, and this leads to:
- Shared routers with many endpoints, where a router‑level dependency unintentionally executes for all of them.
- Dependencies that require parameters, such as
person_id, which may not exist on every route. - Copy‑pasted patterns that hide the fact that dependencies behave differently depending on where they are attached.
- Performance regressions when expensive checks run unnecessarily.
Real-World Impact
This misunderstanding can cause:
- Unnecessary database queries on unrelated endpoints.
- Unexpected 400/404 errors when a dependency expects a path parameter that the route does not provide.
- Hard‑to‑trace authorization failures when auth dependencies are attached too broadly.
- Slower API performance due to redundant dependency execution.
Example or Code (if necessary and relevant)
Below is a minimal example showing the function‑level dependency pattern, which is the safer and more explicit approach:
@router.put("/update-name/{person_id}", status_code=200, response_model=dict)
async def update_name(
person_id: int,
payload: Request,
_: None = Depends(check_person_exists),
):
return {"status": "ok"}
How Senior Engineers Fix It
Senior engineers rely on explicitness and minimal scope:
- Attach dependencies at the function level unless they truly apply to every endpoint.
- Avoid router‑level dependencies that require path parameters, because they create tight coupling.
- Use router‑level dependencies only for cross‑cutting concerns, such as:
- Logging
- Authentication middleware
- Request‑wide metrics
- Document dependency intent so future maintainers understand why it exists.
Why Juniors Miss It
Juniors often miss this issue because:
- FastAPI’s documentation shows both patterns without emphasizing the semantic differences.
- Router‑level dependencies look cleaner, so they seem like the “better” abstraction.
- They assume dependency injection works like decorators, not realizing the scope changes behavior.
- They do not yet recognize the performance and correctness implications of unnecessary dependency execution.
If you’d like, I can also walk through how to refactor an existing FastAPI project to use safer dependency scoping.