Global and Path Dependencies
Individual Depends(...) parameters attach a dependency to one route. FastAPI also lets you attach dependencies to an entire router, to a path prefix, or to the entire application — without touching each route function.
By the end of this lesson you can: attach dependencies to APIRouter and FastAPI globally, use path-level dependencies for selective enforcement, and understand execution order when multiple dependency layers apply.
Router-Level Dependencies
from fastapi import APIRouter, Depends
from app.dependencies.auth import get_current_user, require_role
# Every route in this router requires authentication AND admin role
router = APIRouter(
prefix="/admin",
tags=["admin"],
dependencies=[
Depends(get_current_user),
Depends(require_role("admin")),
],
)
@router.get("/users")
async def list_users() -> list:
return []
@router.delete("/users/{user_id}")
async def delete_user(user_id: int) -> dict:
return {"deleted": user_id}
Neither list_users nor delete_user declares auth in their signatures, yet both are protected. The router-level dependencies list handles it.
Application-Level Dependencies
from fastapi import FastAPI, Depends
from app.dependencies.rate_limit import rate_limit_check
from app.dependencies.logging import log_request
app = FastAPI(
dependencies=[
Depends(rate_limit_check), # Applied to every single route
Depends(log_request),
]
)
App-level dependencies run for every route including health checks. For selective enforcement, prefer router-level or per-route dependencies.
Path-Level Dependencies in include_router
Add dependencies when including a router — without modifying the router source:
from fastapi import FastAPI, Depends
from app.routers import internal
from app.dependencies.network import require_internal_ip
app = FastAPI()
app.include_router(
internal.router,
dependencies=[Depends(require_internal_ip)], # Added at include time
)
Dependency Execution Order
All layers execute in order. If any dependency raises HTTPException, the chain stops and the error is returned immediately.
Logging and Observability Dependencies
A common use of global dependencies is attaching request context:
import time
import logging
from fastapi import Request, Response
from typing import Callable
logger = logging.getLogger("api")
async def log_request(request: Request, response: Response) -> None:
start = time.perf_counter()
logger.info("→ %s %s", request.method, request.url.path)
yield # Pause until after response
elapsed = time.perf_counter() - start
logger.info("← %s %s %.3fs", request.method, request.url.path, elapsed)
log_request uses yield — it is a generator dependency that runs code both before and after the route handler, making it suitable for timing and teardown.
Rate Limiting Dependency
import time
from collections import defaultdict
from fastapi import Request, HTTPException
_counters: dict[str, list[float]] = defaultdict(list)
WINDOW = 60 # seconds
MAX_REQUESTS = 100
async def rate_limit_check(request: Request) -> None:
client_ip = request.client.host
now = time.time()
window_start = now - WINDOW
hits = [t for t in _counters[client_ip] if t > window_start]
if len(hits) >= MAX_REQUESTS:
raise HTTPException(429, detail="Rate limit exceeded. Try again in 60 seconds.")
_counters[client_ip] = hits + [now]
The in-memory rate limiter above is for illustration only. In production, use Redis with a sliding window algorithm to share state across multiple workers.
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| App-level dep blocking health check | All routes authenticated | Move auth to router-level instead of app-level |
| Dependency order wrong | Auth runs after rate limit is too permissive | Order matters — put rate limiting before auth |
| Router dep not inherited | Forgot dependencies=[...] argument | Pass dependencies explicitly to APIRouter() |
| Double-running deps | Same dep at both router and route level | Check for duplicate Depends in both places |
yield dep at app level causing issues | Teardown timing for app-scoped resources | Use lifespan context manager for true app-level resources |
Hands-On Practice
from fastapi import APIRouter
from app.dependencies.auth import get_current_user
from app.dependencies.logging import log_request
# Public router — logging only
public_router = APIRouter(
prefix="/public",
tags=["public"],
dependencies=[Depends(log_request)],
)
# Protected router — logging + auth
protected_router = APIRouter(
prefix="/v1",
tags=["v1"],
dependencies=[Depends(log_request), Depends(get_current_user)],
)
@public_router.get("/status")
async def status() -> dict:
return {"status": "ok"}
@protected_router.get("/items")
async def list_items() -> list:
return [{"id": 1}]
from fastapi import FastAPI
from app.routers.v1.items import public_router, protected_router
from app.dependencies.rate_limit import rate_limit_check
from fastapi import Depends
app = FastAPI(dependencies=[Depends(rate_limit_check)])
app.include_router(public_router)
app.include_router(protected_router)