Skip to content

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?