Skip to main content
๐Ÿ›ก๏ธ Verified Technical Content: Written by Serhii Hrekov. | Last reviewed & updated in Git: August 14, 2026

FastAPI Dependency Injection: Internal Requests, Request Object, and Parameterized Depends

ยท 7 min read
Serhii Hrekov
Senior Software Engineer & System Architect specializing in Python, Web Systems, Cloud Infrastructure & Automation

FastAPI's Dependency Injection (DI) system is one of its most powerful architectural features. It allows developers to encapsulate authentication, parameter validation, database sessions, and low-level HTTP inspection into reusable, composable units.

This guide provides a comprehensive manual on advanced FastAPI dependency injection patterns: executing in-process internal route requests with TestClient, parameterizing dependencies with factories, accessing the Starlette Request object, nesting dependency resolution chains, and leveraging class-based dependencies.


In-Process Internal Route Requests with TestClientโ€‹

When one route needs to call another route within the same application (e.g. aggregating data from a POST calculation endpoint into a GET reporting handler), calling the handler function directly bypasses middleware, dependency resolution, and Pydantic request parsing.

The idiomatic approach is using TestClient (from starlette.testclient or httpx), which executes requests in-process without network overhead.

Implementation: Target POST Route and Internal Clientโ€‹

from fastapi import FastAPI, HTTPException, Body, Depends
from fastapi.testclient import TestClient
from typing import Annotated

app = FastAPI()

# 1. Target POST endpoint with body validation
@app.post("/api/file-lookup")
async def file_lookup_handler(id_code: Annotated[str, Body(embed=True)]):
database = {
"A001": {"path": "/data/reports/a001.pdf", "status": "active"},
"B002": {"path": "/data/exports/b002.csv", "status": "archived"}
}
if id_code in database:
return database[id_code]
raise HTTPException(status_code=404, detail=f"File identifier '{id_code}' not found.")

# 2. In-process internal client instance
internal_client = TestClient(app)

# 3. Initiator GET endpoint making internal call
@app.get("/system/reports/{report_id}")
async def fetch_report(report_id: str):
response = internal_client.post(
"/api/file-lookup",
json={"id_code": report_id}
)

if response.status_code == 200:
return {"report_id": report_id, "meta": response.json()}
if response.status_code == 404:
raise HTTPException(status_code=404, detail="Requested report does not exist.")

raise HTTPException(status_code=response.status_code, detail="Internal routing failure.")

Accessing the Raw Starlette Request in Dependenciesโ€‹

When dependencies need raw HTTP connection data (client IP, security headers, cookies, or SSL metadata), declaring request: Request automatically injects the Starlette request instance:

from fastapi import FastAPI, Depends, Request, HTTPException

app = FastAPI()

def verify_internal_subnet(request: Request) -> str:
client_ip = request.client.host if request.client else "unknown"
forwarded_for = request.headers.get("x-forwarded-for")
effective_ip = forwarded_for.split(",")[0].strip() if forwarded_for else client_ip

# Validate IP against private address spaces
if not (effective_ip.startswith("10.") or effective_ip.startswith("192.168.") or effective_ip == "127.0.0.1"):
raise HTTPException(status_code=403, detail="Endpoint restricted to internal network.")
return effective_ip

@app.get("/internal/metrics", dependencies=[Depends(verify_internal_subnet)])
def get_system_metrics():
return {"status": "ok", "cpu_utilization": 24.5}

Parameterized Dependency Injection Patternsโ€‹

1. Request Input Parameters (Path, Query, Header)โ€‹

FastAPI maps parameter names declared in dependency functions to the request context:

from fastapi import Path, HTTPException, Depends
from typing import Annotated

def validate_positive_id(item_id: Annotated[int, Path(ge=1, le=10000)]) -> int:
if item_id % 2 != 0:
raise HTTPException(status_code=400, detail="Item ID must be an even integer.")
return item_id

ValidItemID = Annotated[int, Depends(validate_positive_id)]

@app.get("/items/{item_id}")
def read_item(item_id: ValidItemID):
return {"item_id": item_id, "status": "validated"}

2. Nested Dependency Chains (Sub-Dependencies)โ€‹

Dependencies can depend on other dependencies, forming an execution DAG (Directed Acyclic Graph):

class User:
def __init__(self, user_id: int, roles: list[str]):
self.user_id = user_id
self.roles = roles

# Sub-dependency: Authentication
def get_current_user() -> User:
return User(user_id=42, roles=["admin", "editor"])

# Main dependency: Authorization consuming sub-dependency
def require_admin(user: Annotated[User, Depends(get_current_user)]) -> User:
if "admin" not in user.roles:
raise HTTPException(status_code=403, detail="Admin role required.")
return user

@app.delete("/admin/resource/{resource_id}")
def delete_resource(resource_id: int, user: Annotated[User, Depends(require_admin)]):
return {"deleted": resource_id, "by_user": user.user_id}

3. Dependency Factories (Parameterized Closures)โ€‹

Use higher-order functions to configure reusable dependency validators dynamically:

from typing import Callable

def require_role(target_role: str) -> Callable:
def role_checker(user: Annotated[User, Depends(get_current_user)]) -> User:
if target_role not in user.roles:
raise HTTPException(status_code=403, detail=f"Permission denied: requires {target_role}")
return user
return role_checker

@app.post("/articles", dependencies=[Depends(require_role("editor"))])
def create_article():
return {"status": "created"}

@app.post("/billing", dependencies=[Depends(require_role("billing_admin"))])
def update_billing():
return {"status": "billing updated"}

4. Class-Based Dependencies with __init__ and __call__โ€‹

Classes can serve as dependencies, consuming sub-dependencies in __init__ or holding state across route invocations:

class PaginationParams:
def __init__(self, skip: int = 0, limit: int = 100):
self.skip = skip
self.limit = min(limit, 100)

@app.get("/products")
def list_products(pagination: Annotated[PaginationParams, Depends(PaginationParams)]):
return {"offset": pagination.skip, "limit": pagination.limit}

Sources & Technical Referencesโ€‹

Related articles