Choosing FastAPI Request Handling: Dependency Injection vs. Middleware
FastAPI offers Dependency Injection and Middleware for request handling. This guide compares both, explains trade‑offs, and walks through a real example of injecting a DB session versus adding a request‑ID header.
31 Mar 2026, 14:33 UTC

Problem Statement
When building a FastAPI service you often need to add cross‑cutting concerns—authentication, logging, request‑ID propagation, or database session management. Two common patterns surface:
- Dependency Injection (DI) – inject services or data directly into the path operation function.
- Middleware – wrap the ASGI application to act before or after every request.
Deciding which technique to use for a particular concern can influence code readability, testability, and performance.
Decision Criteria
Below are the constraints that usually shape the choice:
- Scope – is the functionality needed for every request or only a subset?
- Data Availability – does the logic need path/query parameters, request body, or headers?
- Side‑Effects – does it alter the request/response, or just observe?
- Testing & Debugging – how easily can you unit‑test the component?
- Performance Sensitivity – is the overhead measurable in your latency budget?
Option Comparison
| Feature | Dependency Injection | Middleware |
|---|---|---|
| Scope | Per‑route or per‑group via function parameters | Global for the entire ASGI app |
| Access to request data | Path/query params, headers, body models via type hints | Full ASGI scope; must parse manually |
| Side‑effects | Pure function return values or side‑effects inside dependency | Can modify request/response objects |
| Testability | Inject mocks directly into function signature | Wrap app in test client; harder to isolate |
| OpenAPI impact | Parameters appear automatically in docs | No automatic docs; must document manually |
| Performance | Minimal overhead; resolved at call time | Thin wrapper; negligible but additive |
| Order of execution | Dependencies resolved in order of declaration | Middleware order defined by registration sequence |
Trade‑Offs
- Granularity vs. Simplicity – DI gives fine‑grained control but can clutter the signature if many dependencies are needed. Middleware keeps the route signature clean but may hide the dependency chain.
- Observability – DI automatically surfaces in OpenAPI, making the API contract explicit. Middleware is invisible to the docs generator.
- Debugging Complexity – Overusing DI can obscure the flow for newcomers; middleware can be misordered, causing subtle bugs (e.g., auth running after body consumption).
- Performance – In practice both approaches add <1 ms overhead. Use profiling only if you hit a latency target below 10 ms per request.
Concrete Implementation Example
Scenario: Injecting a Database Session vs. Adding a Request‑ID Header
We’ll build a minimal FastAPI app that demonstrates:
- DI: a per‑request SQLAlchemy session injected into the path operation.
- Middleware: an ASGI middleware that generates a UUID and attaches it to the request state.
# main.py
from fastapi import FastAPI, Depends, Request
from fastapi.responses import JSONResponse
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
import uuid
# --- Dependency Injection: DB session ---------------------------------
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
# --- Middleware: Request ID ---------------------------------------------
class RequestIdMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
request_id = str(uuid.uuid4())
# Attach to scope so downstream can access
scope["request_id"] = request_id
await self.app(scope, receive, send)
app = FastAPI()
app.add_middleware(RequestIdMiddleware)
# --- Route --------------------------------------------------------------
@app.get("/items/{item_id}")
async def read_item(item_id: int, db: Session = Depends(get_db), request: Request = None):
# Use the injected DB session (placeholder logic)
item = {"id": item_id, "name": f"Item {item_id}"}
# Access request‑ID from middleware
request_id = request.scope.get("request_id", "unknown")
return JSONResponse({"item": item, "request_id": request_id})
Run the app with uvicorn main:app --reload and hit /items/42. The JSON response will include the injected database session logic (though we only fabricate an item) and the request‑ID generated by the middleware.
Validation Checklist
- Dependency Resolution – Send a request and confirm the
dbparameter is instantiated (e.g., by logging insideget_db). - OpenAPI Visibility – Open
/docsand verify thatitem_idappears as a path parameter, and that no extra parameters are shown for the middleware. - Middleware Ordering – Add another middleware that reads
request.scope["request_id"]to ensure it runs after the ID middleware. - Performance Check – Use
httpx.AsyncClientto send 1000 requests in a test script and compare elapsed time with and without the middleware. The difference should be within a few milliseconds.
When to Prefer Each Approach
- Use DI when:
- The logic is tightly coupled to a specific route or group.
- You need automatic parameter extraction and validation.
- OpenAPI documentation should reflect the dependency.
- Unit tests can mock the dependency directly via
Dependsinjection.
- Use Middleware when:
- The concern applies to every request (e.g., logging, tracing, CORS, security headers).
- You need to modify the request or response before any route logic runs.
- Order of execution matters across many middlewares (e.g., authentication before rate limiting).
- Performance impact is negligible and you prefer a global, declarative approach.
Conclusion
FastAPI’s DI and middleware systems are complementary. Choose DI for route‑specific, parameter‑aware logic that benefits from automatic validation and OpenAPI integration. Opt for middleware when you need a global, order‑dependent wrapper that can inspect or modify the ASGI scope. By aligning the choice with the constraints outlined above, you keep your codebase clean, testable, and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.