Skip to main content

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.

Learning Focus

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

app/routers/admin.py
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

app/main.py
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),
]
)
warning

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:

app/main.py
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:

app/dependencies/logging.py
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)
note

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

app/dependencies/rate_limit.py
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]
warning

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

PitfallCause / SymptomFix
App-level dep blocking health checkAll routes authenticatedMove auth to router-level instead of app-level
Dependency order wrongAuth runs after rate limit is too permissiveOrder matters — put rate limiting before auth
Router dep not inheritedForgot dependencies=[...] argumentPass dependencies explicitly to APIRouter()
Double-running depsSame dep at both router and route levelCheck for duplicate Depends in both places
yield dep at app level causing issuesTeardown timing for app-scoped resourcesUse lifespan context manager for true app-level resources

Hands-On Practice

app/routers/v1/items.py
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}]
app/main.py
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)

What's Next