Router and App Dependencies
Run a dependency for many routes at once: on one route with dependencies=[...], on every route of an APIRouter, or on the whole app. Protect all /admin routes with one API-key check, see the order in which the levels run, and what the app level does NOT cover.
What you will be able to do
- Run a dependency without using its value: dependencies=[...]
- Group routes with APIRouter and give the group a dependency
- Add dependencies to the whole app
- Know the order of the levels
- Add dependencies when including a router
- Check an API key safely
The idea, in plain English
Some dependencies are checks, not values: "is the API key right?", "write an audit line". The endpoint does not need anything from them - they only have to run, and stop the request if something is wrong. And they are often needed by a whole group of routes, like everything under /admin.
FastAPI lets you attach such dependencies at three levels: one route (dependencies=[...] in the decorator), a router (APIRouter(dependencies=[...]) - every route in it), and the app (FastAPI(dependencies=[...])). Adding a new admin route then cannot forget the check - it is in the group.
We protect an /admin group with an API key, add an audit step and a request log, and test the order and the edges. FastAPI 0.143.0. Module 8 covers routers in big apps; here we use one router.
Worked example: A shop API: public products, an /admin group protected by an X-API-Key header, an audit step on deletes, and a log line for every request.
1 - A public route
/products is not in the router. Only the app dependency runs.
Dependencies run from the outside in. Real results from our shop API.
Words you will see in this lesson
A few words about groups of routes.
APIRouterA group of routes with shared settings: a prefix, tags, dependencies.PrefixThe start of every path in a router, like /admin.include_routerAdds a router’s routes to the app.API keyA secret string a program sends (often in a header) to prove it may call the API.Audit logA record of who changed what, and when.An everyday example: an office building
A building has checks at different levels:
- The front door: everyone who comes in is written in the visitor book - the app level.
- The finance floor: only people with a finance badge get past the floor door - the router level.
- The safe room: you must also sign a sheet - the route level.
- A new finance office does not need its own badge check - the floor door already does it.
Example 1 - an admin group with an API key
Read shop.py from the top:
- log_request(request) prints every request. FastAPI(dependencies=[Depends(log_request)]) runs it for every route.
- verify_api_key reads the X-API-Key header: missing 401, wrong 403. secrets.compare_digest compares without leaking timing information.
- APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(verify_api_key)]) - every route in admin gets the prefix, the tag and the check.
- delete_order also has dependencies=[Depends(audit)] in its decorator - only that route.
- None of the endpoints has a parameter for these checks - dependencies=[...] runs them and throws the return value away.
- app.include_router(admin) adds the group to the app. /products is a normal public route.
import secrets
from typing import Annotated
from fastapi import APIRouter, Depends, FastAPI, Header, HTTPException, Request
ADMIN_KEY = "k-7f3a9c" # in real apps: from settings (Module 10)
def log_request(request: Request): # for the whole app
print(f" app: {request.method} {request.url.path}")
def verify_api_key(x_api_key: Annotated[str | None, Header()] = None):
print(" router: checking X-API-Key")
if x_api_key is None:
raise HTTPException(status_code=401, detail="X-API-Key header missing")
if not secrets.compare_digest(x_api_key, ADMIN_KEY): # compare safely
raise HTTPException(status_code=403, detail="Wrong API key")
def audit(): # for one route
print(" route: audit log written")
app = FastAPI(dependencies=[Depends(log_request)])
admin = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(verify_api_key)])
@admin.get("/stats")
def stats():
print(" endpoint: stats")
return {"orders": 42}
@admin.delete("/orders/{order_id}", dependencies=[Depends(audit)])
def delete_order(order_id: int):
print(" endpoint: delete_order")
return {"deleted": order_id}
app.include_router(admin)
@app.get("/products")
def products():
print(" endpoint: products")
return ["lamp", "chair"]from fastapi.testclient import TestClient
from shop import app
c = TestClient(app)
KEY = {"X-API-Key": "k-7f3a9c"}
for label, method, url, headers in [
("public products", "GET", "/products", {}),
("stats, no key", "GET", "/admin/stats", {}),
("stats, wrong key", "GET", "/admin/stats", {"X-API-Key": "guess"}),
("stats, right key", "GET", "/admin/stats", KEY),
("delete, right key", "DELETE", "/admin/orders/7", KEY),
("unknown path", "GET", "/nothing", {}),
("docs page", "GET", "/docs", {}),
]:
print(label)
r = c.request(method, url, headers=headers)
print(f" -> {r.status_code} {r.text[:60] if 'json' in r.headers['content-type'] else r.headers['content-type']}")Reading the output
- public products - only "app" printed. No key needed.
- stats without a key or with a wrong key - app, then router, then 401 or 403. The endpoint did not run.
- delete with the key - app, router, route, endpoint: from the outside in.
- unknown path - 404 and nothing printed. App dependencies run only when a route matches.
- docs page - 200, and nothing printed. /docs and /openapi.json are not normal routes, so app dependencies do not protect them.
public products
app: GET /products
endpoint: products
-> 200 ["lamp","chair"]
stats, no key
app: GET /admin/stats
router: checking X-API-Key
-> 401 {"detail":"X-API-Key header missing"}
stats, wrong key
app: GET /admin/stats
router: checking X-API-Key
-> 403 {"detail":"Wrong API key"}
stats, right key
app: GET /admin/stats
router: checking X-API-Key
endpoint: stats
-> 200 {"orders":42}
delete, right key
app: DELETE /admin/orders/7
router: checking X-API-Key
route: audit log written
endpoint: delete_order
-> 200 {"deleted":7}
unknown path
-> 404 {"detail":"Not Found"}
docs page
-> 200 text/html; charset=utf-8Watch out: Putting an API-key check on the whole app does not hide /docs. To hide the docs, use docs_url=None and openapi_url=None (Lesson 1.7) - or protect them in front of the app.
In /docs
Both admin routes got the "admin" tag and an x-api-key header parameter - from the router. /products has neither. Clients can see which routes need the key.
docs: GET /admin/stats tags=['admin'] headers=['x-api-key']
docs: DELETE /admin/orders/{order_id} tags=['admin'] headers=['x-api-key']
docs: GET /products tags=[] headers=[]Adding the check when you include a router
Sometimes the router is written without rules - for example in another file, or by someone else. include_router can add a prefix, tags and dependencies when it adds the router:
- reports has no prefix and no dependencies of its own.
- include_router(reports, prefix="/reports", dependencies=[Depends(verify_api_key)]) protected /reports/daily: 401 without a key, 200 with it.
- Use this when the same router is used in two apps with different rules.
from fastapi import APIRouter, Depends, FastAPI
from shop import verify_api_key
reports = APIRouter() # a router with no rules of its own
@reports.get("/daily")
def daily():
return {"sales": 120}
app = FastAPI()
app.include_router(reports, prefix="/reports", tags=["reports"],
dependencies=[Depends(verify_api_key)]) # rules added when including it router: checking X-API-Key
/reports/daily, no key -> 401 {"detail":"X-API-Key header missing"}
router: checking X-API-Key
/reports/daily, key -> 200 {"sales":120}
docs: GET /admin/stats tags=['admin'] headers=['x-api-key']
docs: DELETE /admin/orders/{order_id} tags=['admin'] headers=['x-api-key']
docs: GET /products tags=[] headers=[]Two things we tested
late_route.py adds two routes AFTER include_router:
- /admin/users was added after include_router(admin) - and it worked, with the key check. Older FastAPI versions copied the routes at include time, so many tutorials warn about this. In 0.143.0 it worked - but call include_router at the end anyway; it is easier to read.
- /admin/whoami asks for verify_api_key as a parameter too. It ran once, not twice: the router dependency and the parameter share one result per request (Lesson 4.2).
- The value was null, because verify_api_key returns nothing. If an endpoint needs a value (like the caller’s name), make the dependency return it and ask for it as a parameter.
from typing import Annotated
from fastapi import APIRouter, Depends, FastAPI
from shop import verify_api_key
app = FastAPI()
admin = APIRouter(prefix="/admin", dependencies=[Depends(verify_api_key)])
@admin.get("/stats")
def stats():
return {"orders": 42}
app.include_router(admin)
@admin.get("/users") # added AFTER include_router
def users():
return ["ravi", "anu"]
@admin.get("/whoami")
def whoami(key: Annotated[None, Depends(verify_api_key)]): # the same dependency as a parameter
return {"value": key}/admin/stats
router: checking X-API-Key
-> 200 {"orders":42}
/admin/users
router: checking X-API-Key
-> 200 ["ravi","anu"]
/admin/whoami
router: checking X-API-Key
-> 200 {"value":null}
in /docs: ['/admin/stats', '/admin/users', '/admin/whoami']Which level for which job?
- App level: things for every request - a request log, a request id. Not for login: it would block public routes and health checks.
- Router level: rules for a group - "admins only", "needs an API key", "this partner only".
- Route level: one special step - an audit log on delete, a stricter limit on one endpoint.
- A parameter: when the endpoint needs the value - the current user, a session.
Dependency levels at a glance
One routeValue thrown away.
@app.delete("/x", dependencies=[Depends(audit)])A routerEvery route in it.
APIRouter(prefix="/admin", dependencies=[Depends(verify_api_key)])
When includingRules added from outside.
app.include_router(r, prefix="/reports", dependencies=[...])
The appEvery matched route.
FastAPI(dependencies=[Depends(log_request)])
Safe compareFor secrets.
secrets.compare_digest(given, expected)
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Run fastapi dev shop.py, open /docs and call GET /admin/stats with and without the X-API-Key header.”
“Add a /partners router that needs a different key. Can one route be in both?”
“Make log_request a yield dependency that prints how long each request took (Lesson 4.3).”
“Move verify_api_key to the app level. What happens to /products?”
What usually goes wrong
The return value is thrown away. Ask for it as a parameter if you need it.
It also blocks public routes and health checks. Put it on a router.
They do not run for /docs and /openapi.json.
Use secrets.compare_digest for keys and tokens.
✗ if x_api_key != ADMIN_KEY: ...✓ if not secrets.compare_digest(x_api_key, ADMIN_KEY): ...Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Add a get_api_client dependency that maps keys to client names ({"k-7f3a9c": "back-office", "k-22b1": "warehouse"}) and returns the name. Put it on the admin router, and let /admin/stats return {"orders": 42, "client": name} by asking for the same dependency as a parameter. Check that it still runs once per request.
Show hintHide hint
The router uses dependencies=[Depends(get_api_client)]; the endpoint uses client: Annotated[str, Depends(get_api_client)].
Key points
- dependencies=[...] runs checks whose value you do not need.
- An APIRouter can carry a prefix, tags and dependencies for all its routes.
- include_router can add a prefix and dependencies from outside.
- App dependencies run for every matched route - not for unknown paths or /docs.
- Order: app, router, route, then parameter dependencies, then the endpoint.
- A dependency used at two levels still runs once per request.
Quick check before you move on
Interview questions
How would you protect a group of endpoints with an API key in FastAPI?
Write a dependency that reads the header and compares with secrets.compare_digest, raising 401/403; attach it to an APIRouter (or include_router) via dependencies=[...]. Keep public and health routes outside that router.
Router-level dependencies or middleware?
Dependencies are per-route aware, documented in OpenAPI, typed and overridable in tests; middleware wraps every request including unmatched paths and docs, suiting cross-cutting concerns like CORS, logging or timing.
Quiz
- 1.
In which order did the dependencies of DELETE /admin/orders/7 run?
- 2.
Why does a new route in the admin router need no own key check?
- 3.
Why did nothing print for an unknown path?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...