Unit 07.02: Dependencies that fail the request
A dependency can reject the request before the handler exists.
Declared in the decorator, not the signature
An API key check attached to the route rather than passed to the handler.
The code calls the endpoint three ways and counts handler runs.
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
HANDLER_RAN = []
def require_api_key(x_api_key: str | None = Header(default=None)) -> str:
if x_api_key != "correct-key":
raise HTTPException(status_code=401,
detail={"error": "invalid_api_key"})
return x_api_key
@app.get("/secure", dependencies=[Depends(require_api_key)])
def secure() -> dict:
HANDLER_RAN.append(1)
return {"ok": True}
client = TestClient(app)
print(f"no key -> {client.get('/secure').status_code}")
print(f"wrong key -> {client.get('/secure', headers={'X-API-Key': 'x'}).status_code}")
print(f"correct key -> {client.get('/secure', headers={'X-API-Key': 'correct-key'}).status_code}")
print(f"\nhandler ran {len(HANDLER_RAN)} time(s) out of 3 requests")
# `dependencies=[...]` in the decorator runs the check without passing the
# value to the handler -- the right shape when the handler does not need it.
The handler ran once out of three requests. It never sees the key, because it does not need it - the check is declared where it belongs and the handler stays focused on its own job.
That form is the right shape whenever a dependency guards rather than supplies: the handler should not take a parameter it never uses.
The mistake this prevents
The mistake is passing the key into the handler so it can check it there. The check is then part of the handler's logic, gets copied to the next endpoint, and eventually one copy differs from the others.
Takeaway
Use route-level dependencies for checks that guard rather than supply. The handler stays free of parameters it does not use.
