FastAPI Integration¶
Recipes for integrating dioxide with FastAPI applications.
Recipe: Basic FastAPI Setup¶
Problem¶
You want to set up a FastAPI application with dioxide dependency injection, ensuring proper initialization and cleanup of resources.
Solution¶
Use FastAPI’s lifespan context manager to integrate with dioxide’s container lifecycle.
Code¶
"""FastAPI application with dioxide integration."""
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dioxide import Container, Profile
from fastapi import FastAPI
# Get profile from environment
profile_name = os.getenv("PROFILE", "development")
profile = Profile(profile_name)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Initialize container on startup, cleanup on shutdown."""
async with Container(profile=profile) as container:
# All @lifecycle adapters are now initialized
# Store container for route access
app.state.container = container
yield
# All @lifecycle adapters are now disposed
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "healthy", "profile": profile.value}
Explanation¶
Profile from environment: Read
PROFILEenv var to determine which adapters to useContainer scan: Discovers all
@adapterand@servicedecorated classesLifespan integration: The
async with containerpattern ensures@lifecyclecomponents are properly initialized and disposedClean shutdown: On SIGTERM, FastAPI’s lifespan exits, triggering container cleanup
Why this works with async lifecycle methods: The async with Container(...) pattern calls await container.start() on entry, which runs all initialize() methods before the first request. Resolution via container.resolve() is synchronous and returns already-initialized instances. See Lifecycle Async Patterns for details.
Recipe: Inject Services into Routes¶
Problem¶
You want to inject dioxide services into FastAPI route handlers using FastAPI’s dependency injection.
Solution¶
Create a helper function that resolves from the container, then use Depends().
Code¶
"""Injecting dioxide services into FastAPI routes."""
from typing import Protocol
from dioxide import Container, Profile, adapter, service
from fastapi import Depends, FastAPI
# Domain port
class EmailPort(Protocol):
async def send(self, to: str, subject: str) -> None: ...
# Service depending on port
@service
class NotificationService:
def __init__(self, email: EmailPort) -> None:
self.email = email
async def notify_user(self, user_email: str, message: str) -> None:
await self.email.send(user_email, message)
# Container setup (created during app lifespan, shown here for clarity)
container: Container # Will be set during lifespan
# Dependency injection helper
def get_notification_service() -> NotificationService:
"""Resolve NotificationService from dioxide container."""
return container.resolve(NotificationService)
# FastAPI app
app = FastAPI()
@app.post("/notify/{user_email}")
async def notify_user(
user_email: str,
message: str,
service: NotificationService = Depends(get_notification_service),
) -> dict[str, str]:
"""Send notification to user."""
await service.notify_user(user_email, message)
return {"status": "sent", "to": user_email}
Explanation¶
Helper function:
get_notification_service()wrapscontainer.resolve()Depends(): FastAPI’s standard dependency injection mechanism
Type hints: Full type safety - IDE knows
serviceisNotificationServiceProfile determines adapter: The actual email adapter depends on which profile was scanned
Alternative: Generic resolver factory
from typing import TypeVar
T = TypeVar("T")
def inject(cls: type[T]) -> T:
"""Generic resolver for any dioxide component."""
def _resolve() -> T:
return container.resolve(cls)
return Depends(_resolve)
# Usage
@app.post("/notify/{user_email}")
async def notify_user(
user_email: str,
service: NotificationService = inject(NotificationService),
) -> dict[str, str]:
...
Recipe: Testing FastAPI Endpoints¶
Problem¶
You want to test FastAPI endpoints that use dioxide services, with fast fakes instead of real implementations.
Solution¶
Set the TEST profile before importing the app, then use FastAPI’s TestClient.
Code¶
"""Testing FastAPI endpoints with dioxide fakes."""
import os
import pytest
from fastapi.testclient import TestClient
# Set TEST profile BEFORE importing the app
os.environ["PROFILE"] = "test"
from app.main import app, container
from app.domain.ports import EmailPort
@pytest.fixture
def client() -> TestClient:
"""Create test client."""
return TestClient(app)
@pytest.fixture
def fake_email():
"""Get fake email adapter for verification."""
return container.resolve(EmailPort)
@pytest.fixture(autouse=True)
def clear_fakes(fake_email):
"""Clear fake state before each test."""
fake_email.sent_emails.clear()
yield
class DescribeNotifyEndpoint:
"""Tests for POST /notify endpoint."""
def it_sends_notification_email(self, client, fake_email):
"""Sends email when notification requested."""
response = client.post(
"/notify/alice@example.com",
params={"message": "Hello!"},
)
assert response.status_code == 200
assert response.json()["status"] == "sent"
# Verify email was sent via fake
assert len(fake_email.sent_emails) == 1
assert fake_email.sent_emails[0]["to"] == "alice@example.com"
def it_returns_400_for_invalid_email(self, client, fake_email):
"""Returns error for invalid email format."""
response = client.post(
"/notify/not-an-email",
params={"message": "Hello!"},
)
assert response.status_code == 400
assert len(fake_email.sent_emails) == 0
Explanation¶
Profile before import: Set
PROFILE=testbefore importing app so container scans TEST adaptersTestClient: FastAPI’s test client handles lifespan automatically
Fake verification: Access fake adapters to verify side effects
Clear state: Reset fakes between tests for isolation
BDD naming: Use
Describe*andit_*pattern for clear test names
Recipe: Custom Middleware with dioxide¶
Problem¶
You want to access dioxide services from custom FastAPI middleware.
Solution¶
Access the container directly in middleware (it’s a module-level singleton).
Code¶
"""Custom middleware using dioxide services."""
import time
from typing import Protocol
from dioxide import Container, Profile, adapter, service
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
# Metrics port
class MetricsPort(Protocol):
def record_request(self, path: str, duration_ms: float) -> None: ...
# Production metrics adapter
@adapter.for_(MetricsPort, profile=Profile.PRODUCTION)
class DatadogMetricsAdapter:
def record_request(self, path: str, duration_ms: float) -> None:
# Send to Datadog
pass
# Test fake
@adapter.for_(MetricsPort, profile=Profile.TEST)
class FakeMetricsAdapter:
def __init__(self):
self.recorded: list[dict] = []
def record_request(self, path: str, duration_ms: float) -> None:
self.recorded.append({"path": path, "duration_ms": duration_ms})
# Container (created during app lifespan)
container: Container # Will be set during lifespan
class MetricsMiddleware(BaseHTTPMiddleware):
"""Middleware that records request metrics."""
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration_ms = (time.perf_counter() - start) * 1000
# Access dioxide service
metrics = container.resolve(MetricsPort)
metrics.record_request(request.url.path, duration_ms)
return response
app = FastAPI()
app.add_middleware(MetricsMiddleware)
Explanation¶
Module-level container: Middleware can access the same container instance
Resolve per request: Each request resolves fresh (though singletons return same instance)
Profile determines implementation: TEST profile uses fake that captures metrics for testing
Metrics verification in tests: Access fake to verify metrics were recorded
Recipe: Background Tasks with dioxide¶
Problem¶
You want to use dioxide services in FastAPI background tasks.
Solution¶
Resolve services within the background task function (container is available).
Code¶
"""Background tasks with dioxide services."""
from typing import Protocol
from dioxide import Container, Profile, adapter, service
from fastapi import BackgroundTasks, FastAPI
# Email port
class EmailPort(Protocol):
async def send(self, to: str, subject: str, body: str) -> None: ...
@service
class EmailService:
def __init__(self, email: EmailPort) -> None:
self.email = email
async def send_welcome(self, user_email: str, name: str) -> None:
await self.email.send(
to=user_email,
subject="Welcome!",
body=f"Hello {name}, welcome to our service!",
)
# Container (created during app lifespan)
container: Container # Will be set during lifespan
async def send_welcome_email_task(user_email: str, name: str) -> None:
"""Background task to send welcome email."""
# Resolve service inside the task
email_service = container.resolve(EmailService)
await email_service.send_welcome(user_email, name)
app = FastAPI()
@app.post("/users")
async def create_user(
name: str,
email: str,
background_tasks: BackgroundTasks,
) -> dict[str, str]:
"""Create user and send welcome email in background."""
# Create user synchronously
user_id = "123" # From your database
# Queue background email
background_tasks.add_task(send_welcome_email_task, email, name)
return {"id": user_id, "name": name, "email": email}
Explanation¶
Resolve in task: Background tasks run after response, but container is still available
Async task: Use
async deffor background tasks that use async servicesSame container: Background task uses same container, same singletons
Testing: With TEST profile, fake email captures what would be sent
See Also¶
Lifecycle Methods: Async/Sync Patterns - Understanding async lifecycle with sync resolution
Scoping Guide - Request scoping for per-request isolation
Testing Patterns - More testing recipes
Configuration - Environment-specific config with Pydantic Settings
Getting Started - dioxide fundamentals