← Back to FastAPI
Lesson 4.4 · Dependencies

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.

Intermediate25 min

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.

workflowThree levelsstep 1 / 3

1 - A public route

/products is not in the router. Only the app dependency runs.

GET /products
200
key needed
no

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.

Small dictionary
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.
Example 1 - shop.py
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"]
try_shop.py
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.
Output
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-8

Watch 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.

Output - from app.openapi()
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.
include_deps.py
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
Output
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.
late_route.py
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}
Output
/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 route

Value thrown away.

@app.delete("/x", dependencies=[Depends(audit)])
A router

Every route in it.

APIRouter(prefix="/admin", dependencies=[Depends(verify_api_key)])
When including

Rules added from outside.

app.include_router(r, prefix="/reports", dependencies=[...])
The app

Every matched route.

FastAPI(dependencies=[Depends(log_request)])
Safe compare

For 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.

Use /docs

“Run fastapi dev shop.py, open /docs and call GET /admin/stats with and without the X-API-Key header.”

A second router

“Add a /partners router that needs a different key. Can one route be in both?”

Add a timer

“Make log_request a yield dependency that prints how long each request took (Lesson 4.3).”

Move the check

“Move verify_api_key to the app level. What happens to /products?”

What usually goes wrong

Expecting a value from dependencies=[...]

The return value is thrown away. Ask for it as a parameter if you need it.

Login on the whole app

It also blocks public routes and health checks. Put it on a router.

Thinking app dependencies hide /docs

They do not run for /docs and /openapi.json.

Comparing secrets with ==

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.

1.

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 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

What printed for GET /products?
Only the app log line, then the endpoint.
Did log_request run for /docs?
No - /docs is not a normal route.
What did /admin/stats answer with a wrong key?
403 Wrong API key.
What value did /admin/whoami get from verify_api_key?
null - the dependency returns nothing.

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. 1.

    In which order did the dependencies of DELETE /admin/orders/7 run?

  2. 2.

    Why does a new route in the admin router need no own key check?

  3. 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...