FastAPI boundary

FastAPI Depends adapts HTTP. The app owns service wiring.

FastAPI Depends is excellent at the request boundary. Injex solves the separate problem of application service wiring reused by FastAPI, Typer, workers, scripts, and tests.

Use FastAPI Depends for

Use plain factories or Injex for

Recommended boundary

def build_services(settings: Settings) -> Services:
    container = Container()
    container.add_instance(Settings, settings)
    container.add_singleton(ApiClient)
    container.add_transient(UserRepository)
    container.add_transient(RegisterUser)
    container.assert_valid()
    return Services(container)

FastAPI adapts the graph in lifespan and request dependencies:

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.services = build_services(load_settings())
    yield

def get_register_user(request: Request) -> RegisterUser:
    return request.app.state.services.register_user

Workers and CLIs use the same builder directly:

services = build_services(load_settings())
services.register_user.execute("ada@example.com")

Rule of thumb: FastAPI adapts HTTP. The application owns service wiring.

Optional integration

If you'd rather not write the lifespan/scope glue by hand, the optional injex.ext.fastapi integration (install injex[fastapi]) opens one scope per request, finalizes its resources when the request ends, and provides a Provide dependency:

from injex.ext.fastapi import Provide, setup_injex

setup_injex(app, container)

@app.post("/register")
def register_user(email: str, use_case: RegisterUser = Provide(RegisterUser)) -> int:
    return use_case.execute(email)

It is a thin adapter over ascope(); the container stays framework-agnostic, so the same wiring still serves workers, CLIs, and tests.