FastAPI Integration¶
-
Clean Injection
Declare dependencies by using type annotations. No
Depends()chains required. -
Zero Runtime Overhead
Inject dependencies with zero runtime overhead using Class-Based Handlers.
-
Access Anywhere
Retrieve the container in middleware, decorators, and other places where FastAPI's DI can't reach.
-
Framework-Agnostic
Share your service layer with CLI tools, background workers, and other frameworks.
Quick Start¶
Here is a complete, copy-pasteable example to get you running in under 2 minutes.
Create an async container, define your services, then initialize the integration by calling
wireup.integration.fastapi.setup after adding all routers:
import wireup
from fastapi import FastAPI
from wireup import injectable, Injected
import wireup.integration.fastapi
# 1. Define a service (add @injectable)
@injectable
class GreeterService:
def greet(self, name: str) -> str:
return f"Hello, {name}!"
# 2. Create the container
container = wireup.create_async_container(injectables=[GreeterService])
# 3. Create the FastAPI app and define your routes
app = FastAPI()
@app.get("/")
async def greet(greeter: Injected[GreeterService]):
return {"message": greeter.greet("World")}
# 4. Initialize Wireup (after all routes are added)
wireup.integration.fastapi.setup(container, app)
Run the server with:
fastapi dev main.py
See how Wireup compares to Depends()
This comparison shows the boilerplate reduction when using Wireup's type-based injection versus Depends() chains.
# Define your services
class UserRepository:
def __init__(self, db: Database) -> None:
self.db = db
class UserService:
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
# Create factory functions for each dependency
def get_db() -> Database: ...
def get_user_repo(db: Annotated[Database, Depends(get_db)]) -> UserRepository:
return UserRepository(db)
def get_user_service(
repo: Annotated[UserRepository, Depends(get_user_repo)],
) -> UserService:
return UserService(repo)
# Wire up the dependency chain in the route
@app.get("/users")
async def list_users(
service: Annotated[UserService, Depends(get_user_service)],
):
return service.find_all()
# Add @injectable
@injectable
class UserRepository:
def __init__(self, db: Database) -> None:
self.db = db
@injectable
class UserService:
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
# Inject directly by type
@app.get("/users")
async def list_users(service: Injected[UserService]):
return service.find_all()
Features¶
HTTP and WebSocket Injection¶
Inject dependencies in HTTP routes and WebSockets.
from typing import Annotated
from fastapi import Depends
from wireup import Injected, Inject
@app.get("/random")
async def target(
# Inject custom services
random_service: Injected[RandomService],
# Inject configuration values
is_debug: Annotated[bool, Inject(config="debug")],
# You can still use regular FastAPI dependencies alongside Wireup
user_agent: Annotated[str | None, Header()] = None,
): ...
from fastapi import WebSocket
from wireup import Injected
@app.websocket("/ws")
async def ws(websocket: WebSocket, greeter: Injected[GreeterService]): ...
Class-Based Handlers (Zero Overhead)¶
For the best performance and organization, use Class-Based Handlers. Dependencies injected into the constructor are resolved only once at startup, removing the overhead of dependency resolution from the request cycle entirely.
class UserHandler:
router = fastapi.APIRouter()
# Injected ONCE at startup (Zero runtime cost)
def __init__(self, user_service: UserProfileService) -> None:
self.user_service = user_service
@router.get("/")
async def list_all(self):
return self.user_service.find_all()
Read the Class-Based Handlers guide (similar to @cbv from
fastapi-utils, but with zero per-request overhead)
Performance Tip: Use WireupRoute
Improve performance in function-based routes by using a custom APIRoute class. This reduces overhead in endpoints
that use Wireup injection by avoiding redundant processing.
from fastapi import APIRouter
from wireup.integration.fastapi import WireupRoute
router = APIRouter(route_class=WireupRoute)
Under the hood: FastAPI processes all route parameters, including ones meant for Wireup. The WireupRoute class
optimizes this by making Wireup-specific parameters only visible to Wireup, removing unnecessary processing by FastAPI's
dependency injection system.
Injecting Request & WebSocket¶
To inject the Request or WebSocket object into your scoped-lifetime services (e.g. for logging or auth), add
wireup.integration.fastapi to your container and request fastapi.Request or fastapi.WebSocket in your
dependencies.
import wireup
import wireup.integration.fastapi
container = wireup.create_async_container(
# Add the integration module to injectables
injectables=[services, wireup.integration.fastapi],
)
import fastapi
@injectable(lifetime="scoped")
class HttpAuthenticationService:
def __init__(self, request: fastapi.Request) -> None: ...
Testing¶
For general testing tips with Wireup refer to the test docs. With the FastAPI integration, you can override dependencies in the container as follows.
from wireup.integration.fastapi import get_app_container
def test_override(client):
class DummyGreeter(GreeterService):
def greet(self, name: str) -> str:
return f"Hi, {name}"
with get_app_container(app).override.injectable(
GreeterService,
new=DummyGreeter(),
):
res = client.get("/greet?name=Test")
Pitfall: Why lru_cache leaks state in tests
In standard FastAPI applications, singletons are often implemented using @lru_cache. This can cause state to leak
between tests because the cache persists globally in memory.
# Standard FastAPI
@lru_cache
def get_settings():
return Settings()
def test_one():
# Modifies the cached settings instance
get_settings().debug = True
def test_two():
# FAILS: This test inherits the modified state from test_one!
assert get_settings().debug is False
Wireup avoids this automatically.
When you create a fresh container/app for each test (via a pytest fixture), Wireup creates fresh instances of all your services. There is no global cache to clear.
See FastAPI integration tests for more examples.
Warning
FastAPI's lifespan events are required to close the Wireup container properly. Use a context manager when instantiating the test client if using class-based handlers or generator factories in the application.
@pytest.fixture()
def client(app: FastAPI) -> Iterator[TestClient]:
with TestClient(app) as client:
yield client