FastAPI Integration¶
FastAPI integration provides seamless dependency injection for FastAPI applications, enabling automatic service resolution with proper request scoping and lifecycle management.
๐ฏ Getting Started¶
Basic Setup¶
from fastapi import FastAPI
from injectq import InjectQ
from injectq.integrations.fastapi import setup_fastapi_integration, InjectQDependency
# 1. Create container and bind services
container = InjectQ()
container.bind(IUserService, UserService())
container.bind(IOrderService, OrderService())
# 2. Create FastAPI app
app = FastAPI(title="My API", version="1.0.0")
# 3. Set up integration
setup_fastapi_integration(app, container)
# 4. Use dependency injection in endpoints
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: IUserService = InjectQDependency(IUserService)
):
return user_service.get_user(user_id)
@app.post("/orders")
async def create_order(
order_data: OrderCreate,
order_service: IOrderService = InjectQDependency(IOrderService)
):
return order_service.create_order(order_data)
Service Definitions¶
from typing import Protocol
# Define service interfaces
class IUserService(Protocol):
def get_user(self, user_id: int) -> User: ...
def create_user(self, user_data: UserCreate) -> User: ...
class IOrderService(Protocol):
def create_order(self, order_data: OrderCreate) -> Order: ...
def get_order(self, order_id: int) -> Order: ...
# Implement services
class UserService:
def __init__(self, db: IDatabaseConnection):
self.db = db
def get_user(self, user_id: int) -> User:
return self.db.query(User).filter(id=user_id).first()
def create_user(self, user_data: UserCreate) -> User:
user = User(**user_data.dict())
self.db.add(user)
self.db.commit()
return user
class OrderService:
def __init__(self, db: IDatabaseConnection, user_service: IUserService):
self.db = db
self.user_service = user_service
def create_order(self, order_data: OrderCreate) -> Order:
# Validate user exists
user = self.user_service.get_user(order_data.user_id)
order = Order(**order_data.dict())
self.db.add(order)
self.db.commit()
return order
def get_order(self, order_id: int) -> Order:
return self.db.query(Order).filter(id=order_id).first()
๐ง Advanced Configuration¶
Custom Container Setup¶
from injectq import InjectQ, Module
class ApplicationModule(Module):
def __init__(self, config: AppConfig):
self.config = config
def configure(self, binder):
# Database
binder.bind(IDatabaseConnection, create_database_connection(self.config.database))
# Services
binder.bind(IUserService, UserService())
binder.bind(IOrderService, OrderService())
# External services
binder.bind(IEmailService, SmtpEmailService(self.config.email))
binder.bind(IPaymentService, StripePaymentService(self.config.payment))
def create_app(config: AppConfig) -> FastAPI:
# Create container with modules
container = InjectQ()
container.install(ApplicationModule(config))
# Create FastAPI app
app = FastAPI(
title=config.app_name,
version=config.version,
debug=config.debug
)
# Set up integration
setup_fastapi_integration(app, container)
return app
# Usage
config = AppConfig.from_env()
app = create_app(config)
Environment-Specific Setup¶
def create_container_for_env(env: str) -> InjectQ:
container = InjectQ()
if env == "production":
container.install(ProductionDatabaseModule())
container.install(RedisCacheModule())
container.install(SmtpEmailModule())
elif env == "testing":
container.install(TestDatabaseModule())
container.install(InMemoryCacheModule())
container.install(MockEmailModule())
else: # development
container.install(DevDatabaseModule())
container.install(InMemoryCacheModule())
container.install(ConsoleEmailModule())
return container
def create_app() -> FastAPI:
env = os.getenv("ENV", "development")
container = create_container_for_env(env)
app = FastAPI()
setup_fastapi_integration(app, container)
return app
๐จ Dependency Injection Patterns¶
Constructor Injection¶
# Services with dependencies
@singleton
class DatabaseConnection:
def __init__(self, config: DatabaseConfig):
self.config = config
self.connection = create_connection(config)
@scoped
class UserService:
def __init__(self, db: IDatabaseConnection, cache: ICache):
self.db = db
self.cache = cache
# Bind in module
class ServiceModule(Module):
def configure(self, binder):
binder.bind(DatabaseConfig, DatabaseConfig.from_env())
binder.bind(IDatabaseConnection, DatabaseConnection())
binder.bind(ICache, RedisCache())
binder.bind(IUserService, UserService())
# Use in endpoints
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: IUserService = InjectQDependency(IUserService)
):
return user_service.get_user(user_id)
Request-Scoped Services¶
@scoped
class RequestContext:
def __init__(self):
self.request_id = str(uuid.uuid4())
self.start_time = time.time()
self.user_id = None
def set_user(self, user_id: int):
self.user_id = user_id
self.request_time = time.time() - self.start_time
@scoped
class RequestCache:
def __init__(self):
self.data = {}
def get(self, key: str):
return self.cache.get(key)
def set(self, key: str, value):
self.cache[key] = value
# Automatic request scoping
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
ctx: RequestContext = InjectQDependency(RequestContext),
cache: RequestCache = InjectQDependency(RequestCache),
user_service: IUserService = InjectQDependency(IUserService)
):
ctx.set_user(user_id) # Context is scoped to this request
# Cache is also scoped to this request
cache_key = f"user:{user_id}"
user = cache.get(cache_key)
if user is None:
user = user_service.get_user(user_id)
cache.set(cache_key, user)
return user
Authentication Integration¶
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Depends, HTTPException
security = HTTPBearer()
@singleton
class AuthService:
def __init__(self, jwt_secret: str):
self.jwt_secret = jwt_secret
def verify_token(self, token: str) -> User:
try:
payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
return User(id=payload["user_id"], email=payload["email"])
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
auth_service: IAuthService = InjectQDependency(IAuthService)
) -> User:
return auth_service.verify_token(credentials.credentials)
@app.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
return current_user
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
current_user: User = Depends(get_current_user),
user_service: IUserService = InjectQDependency(IUserService)
):
# current_user is authenticated user from JWT
# user_service is injected from container
if current_user.id != user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail="Not authorized")
return user_service.get_user(user_id)
๐งช Testing FastAPI Integration¶
Unit Testing Endpoints¶
import pytest
from fastapi.testclient import TestClient
from injectq.integrations.fastapi import setup_fastapi_integration
@pytest.fixture
def test_app():
# Create test container
container = InjectQ()
container.bind(IUserService, MockUserService())
container.bind(IOrderService, MockOrderService())
# Create test app
app = FastAPI()
setup_fastapi_integration(app, container)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: IUserService = InjectQDependency(IUserService)
):
return user_service.get_user(user_id)
return app
def test_get_user(test_app):
client = TestClient(test_app)
response = client.get("/users/123")
assert response.status_code == 200
assert response.json()["id"] == 123
def test_request_scoping(test_app):
client = TestClient(test_app)
# Each request should be isolated
response1 = client.get("/users/1")
response2 = client.get("/users/2")
# Both should succeed (no state leakage)
assert response1.status_code == 200
assert response2.status_code == 200
Integration Testing¶
@pytest.fixture
def integration_app():
# Real container with test database
container = InjectQ()
container.install(TestDatabaseModule())
container.install(UserModule())
container.install(OrderModule())
app = FastAPI()
setup_fastapi_integration(app, container)
@app.post("/users")
async def create_user(
user_data: UserCreate,
user_service: IUserService = InjectQDependency(IUserService)
):
return user_service.create_user(user_data)
@app.post("/orders")
async def create_order(
order_data: OrderCreate,
order_service: IOrderService = InjectQDependency(IOrderService)
):
return order_service.create_order(order_data)
return app
def test_user_order_workflow(integration_app):
client = TestClient(integration_app)
# Create user
user_response = client.post("/users", json={
"name": "Test User",
"email": "test@example.com"
})
assert user_response.status_code == 201
user_id = user_response.json()["id"]
# Create order for user
order_response = client.post("/orders", json={
"user_id": user_id,
"items": [{"product_id": 1, "quantity": 2}]
})
assert order_response.status_code == 201
order = order_response.json()
assert order["user_id"] == user_id
Mock Testing¶
class MockUserService:
def __init__(self):
self.users = {}
self.call_count = 0
def get_user(self, user_id: int):
self.call_count += 1
return self.users.get(user_id, {"id": user_id, "name": "Mock User"})
def create_user(self, user_data):
user_id = len(self.users) + 1
user = {"id": user_id, **user_data.dict()}
self.users[user_id] = user
return user
def test_with_mocks():
container = InjectQ()
mock_user_service = MockUserService()
container.bind(IUserService, mock_user_service)
app = FastAPI()
setup_fastapi_integration(app, container)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: IUserService = InjectQDependency(IUserService)
):
return user_service.get_user(user_id)
client = TestClient(app)
# Test endpoint
response = client.get("/users/123")
assert response.status_code == 200
# Verify mock was called
assert mock_user_service.call_count == 1
๐จ Common Patterns and Pitfalls¶
โ Good Patterns¶
1. Proper Scoping¶
# โ
Good: Use scoped for request-specific data
@scoped
class RequestMetrics:
def __init__(self):
self.start_time = time.time()
self.queries = []
def record_query(self, query: str, duration: float):
self.queries.append({"query": query, "duration": duration})
# โ
Good: Use singleton for shared resources
@singleton
class DatabasePool:
def __init__(self, config: DatabaseConfig):
self.pool = create_pool(config)
# โ
Good: Use transient for stateless operations
@transient
class PasswordHasher:
def hash(self, password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt())
2. Error Handling¶
# โ
Good: Handle service errors gracefully
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: IUserService = InjectQDependency(IUserService)
):
try:
return user_service.get_user(user_id)
except UserNotFoundError:
raise HTTPException(status_code=404, detail="User not found")
except ServiceError:
raise HTTPException(status_code=500, detail="Internal server error")
3. Middleware Integration¶
# โ
Good: Use middleware for cross-cutting concerns
from fastapi import Request
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
start_time = time.time()
# Get request-scoped logger if available
try:
logger = get_request_container(request).get(ILogger)
logger.info(f"Request started: {request.method} {request.url}")
except:
pass # Logger not available, continue
response = await call_next(request)
duration = time.time() - start_time
print(f"Request completed in {duration:.2f}s")
return response
โ Bad Patterns¶
1. Manual Container Access¶
# โ Bad: Manual container access in endpoints
container = InjectQ() # Global container
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user_service = container.get(IUserService) # Manual resolution
return user_service.get_user(user_id)
# โ
Good: Use dependency injection
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: IUserService = InjectQDependency(IUserService)
):
return user_service.get_user(user_id)
2. Singleton Abuse¶
# โ Bad: Singleton for request-specific data
@singleton
class CurrentUser:
def __init__(self):
self.user = None
def set_user(self, user):
self.user = user # Shared across requests!
# โ Bad: Singleton for mutable state
@singleton
class RequestCache:
def __init__(self):
self.data = {} # Shared and accumulates forever!
# โ
Good: Scoped for request-specific data
@scoped
class CurrentUser:
def __init__(self):
self.user = None
@scoped
class RequestCache:
def __init__(self):
self.data = {} # Isolated per request
3. Heavy Services per Request¶
# โ Bad: Heavy service per request
@transient
class MLModelService:
def __init__(self):
self.model = load_ml_model() # 2GB model loaded per request!
# โ
Good: Singleton for heavy resources
@singleton
class MLModelService:
def __init__(self):
self.model = load_ml_model() # Loaded once
def predict(self, data):
return self.model.predict(data)
โก Advanced Features¶
Custom Dependency Resolver¶
from injectq.integrations.fastapi import InjectQDependencyResolver
class CustomResolver(InjectQDependencyResolver):
def resolve_dependency(self, dependency_type: Type[T]) -> T:
# Custom resolution logic
if dependency_type == ISpecialService:
# Create special service with custom logic
return SpecialServiceImpl(custom_config)
# Fall back to container resolution
return super().resolve_dependency(dependency_type)
# Use custom resolver
setup_fastapi_integration(app, container, resolver=CustomResolver())
Request Container Access¶
from injectq.integrations.fastapi import get_request_container
@app.get("/debug")
async def debug_endpoint(request: Request):
# Get the request-scoped container
request_container = get_request_container(request)
# Access request-scoped services
ctx = request_container.get(RequestContext)
cache = request_container.get(RequestCache)
return {
"request_id": ctx.request_id,
"cache_size": len(cache.data),
"services": list(request_container._bindings.keys())
}
Background Tasks Integration¶
from fastapi import BackgroundTasks
@singleton
class BackgroundTaskService:
def __init__(self, email_service: IEmailService):
self.email_service = email_service
async def send_welcome_email(self, user_email: str):
await self.email_service.send_email(
to=user_email,
subject="Welcome!",
body="Welcome to our platform!"
)
@app.post("/users")
async def create_user(
user_data: UserCreate,
background_tasks: BackgroundTasks,
user_service: IUserService = InjectQDependency(IUserService),
task_service: BackgroundTaskService = InjectQDependency(BackgroundTaskService)
):
# Create user
user = user_service.create_user(user_data)
# Send welcome email in background
background_tasks.add_task(
task_service.send_welcome_email,
user.email
)
return user
WebSocket Support¶
from fastapi import WebSocket
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(
websocket: WebSocket,
client_id: str,
ws_service: IWebSocketService = InjectQDependency(IWebSocketService)
):
await websocket.accept()
# WebSocket connection gets its own scoped services
ctx = websocket.scope.get("injectq_container")
if ctx:
# Services are scoped to this WebSocket connection
session_service = ctx.get(ISessionService)
session_service.set_client_id(client_id)
while True:
data = await websocket.receive_text()
# Handle WebSocket messages with injected services
response = ws_service.process_message(client_id, data)
await websocket.send_text(response)
๐ฏ Summary¶
FastAPI integration provides:
- Automatic dependency injection - No manual container management
- Request-scoped services - Proper isolation per HTTP request
- Type-driven injection - Just add type hints to endpoint parameters
- Framework lifecycle integration - Automatic cleanup and resource management
- Testing support - Easy mocking and test isolation
Key features: - Seamless integration with FastAPI's dependency system - Support for all InjectQ scopes (singleton, scoped, transient) - Request-scoped container access - Custom dependency resolvers - Background task integration - WebSocket support
Best practices: - Use scoped services for request-specific data - Use singleton for shared resources and heavy objects - Use transient for stateless operations - Handle errors gracefully in endpoints - Test thoroughly with mocked dependencies - Avoid manual container access in endpoints
Ready to explore Taskiq integration?