DI extension for FastAPIEX
Project description
fastapiex-di
Production-ready FastAPI extension for service registry and dependency injection.
Installation
uv add fastapiex-di
Quick Start
from fastapi import FastAPI
from fastapiex.di import BaseService, Inject, Service, install_di
app = FastAPI()
install_di(
app,
service_packages=["myapp.services"],
)
@Service("clock_service", eager=True)
class ClockService(BaseService):
@classmethod
async def create(cls) -> "ClockService":
return cls()
@app.get("/health")
async def health(svc: ClockService = Inject("clock_service")):
return {"ok": isinstance(svc, ClockService)}
install_di(...) wires startup/shutdown lifecycle automatically.
Important:
- Keep service modules lazily imported by
install_di(...)package scanning. - Avoid importing decorated service modules before
install_di(...)runs.
Quickstart
Use this exact structure from your project root:
base_dir/
├── .venv/
└── demo/
├── __init__.py
├── main.py
└── services.py
demo/services.py:
from fastapiex.di import BaseService, Service
@Service("ping_service")
class PingService(BaseService):
@classmethod
async def create(cls) -> "PingService":
return cls()
async def ping(self) -> str:
return "pong"
demo/main.py:
from fastapi import FastAPI
from fastapiex.di import Inject, install_di
app = FastAPI()
install_di(app, service_packages=["demo.services"])
@app.get("/ping")
async def ping(svc=Inject("ping_service")):
return {"msg": await svc.ping()}
Run:
uv run uvicorn demo.main:app --reload
Then open http://127.0.0.1:8000/ping and expect:
{"msg":"pong"}
Notes:
service_packages=["demo.services"]must be a real import path, not a filesystem path.- Do not import
demo.servicesindemo/main.py; letinstall_di(...)import it during startup.
Why Not Single-File
@Service and @ServiceDict run at import time.
The registration capture window is opened during install_di(...) startup import scanning.
If decorated services are imported before that window, startup fails with:
No active app service registry capture.
Use a separate service module (for example demo/services.py) and point
service_packages to that module path.
Import Timing Rules
| Do | Don't |
|---|---|
install_di(app, service_packages=["demo.services"]) |
Put @Service classes in main.py and import them before startup |
| Keep decorated services under a dedicated module/package | from demo.services import PingService in demo/main.py |
| Let DI scan import service modules during startup | Manually import decorated service modules in app bootstrap |
Project Layout Contract
service_packages accepts Python import paths, not filesystem paths.
Examples:
- Valid:
service_packages=["demo.services"] - Valid:
service_packages=["myapp.services"] - Invalid:
service_packages=["demo/services.py"] - Invalid:
service_packages=["./demo/services"]
Ideal App Layout
Example project structure that keeps DI wiring predictable:
myapp/
├── app/
│ ├── main.py
│ ├── core/
│ │ ├── settings.py
│ │ └── logging.py
│ ├── api/
│ │ ├── __init__.py
│ │ └── v1/
│ │ ├── __init__.py
│ │ └── users.py
│ └── services/
│ ├── __init__.py
│ ├── database.py
│ ├── cache.py
│ └── user_repo.py
└── pyproject.toml
app/main.py:
from fastapi import FastAPI
from fastapiex.di import install_di
app = FastAPI()
install_di(app, service_packages=["app.services"])
Guidelines:
- Keep all
@Service/@ServiceDictclasses under one or more explicit packages (for exampleapp.services). - Keep route handlers under
app.api.*, and resolve dependencies viaInject(...)only. - Keep framework config (
settings, logging, middleware wiring) underapp.core.*.
Service Registration
1. Named service
from fastapiex.di import BaseService, Service
@Service("user_service")
class UserService(BaseService):
@classmethod
async def create(cls) -> "UserService":
return cls()
2. Anonymous service (type-only)
from fastapiex.di import BaseService, Service
class UserCache:
pass
@Service
class UserCacheProvider(BaseService):
@classmethod
async def create(cls) -> UserCache:
return UserCache()
3. ServiceDict expansion
from fastapiex.di import BaseService, ServiceDict
@ServiceDict("{}_db_service", dict={"main": {"dsn": "sqlite+aiosqlite:///main.db"}})
class DatabaseService(BaseService):
@classmethod
async def create(cls, dsn: str) -> "DatabaseService":
instance = cls()
instance.dsn = dsn
return instance
Declaring Service-to-Service Dependencies
Use require(...) in create(...) defaults.
from fastapiex.di import BaseService, Service, require
@Service("repo_service")
class RepoService(BaseService):
@classmethod
async def create(cls, db=require("main_db_service")) -> "RepoService":
_ = db
return cls()
You can also depend on type:
@Service("consumer")
class ConsumerService(BaseService):
@classmethod
async def create(cls, cache=require(UserCache)) -> "ConsumerService":
_ = cache
return cls()
Injecting Services in FastAPI Endpoints
Key-based
from fastapiex.di import Inject
@app.get("/users")
async def list_users(repo=Inject("repo_service")):
return {"ok": True}
Type-based (only when exactly one provider exists)
@app.get("/cache")
async def cache_state(cache: UserCache = Inject(UserCache)):
return {"ok": isinstance(cache, UserCache)}
Production Settings
install_di(...) options:
service_packages: package(s) to scan for decorated services.strict(defaultTrue): fail startup on DI/registry errors.allow_private_modules(defaultFalse): include modules with underscore segments.auto_add_finalizer_middleware(defaultTrue): auto install transient cleanup middleware.freeze_container_after_startup(defaultTrue): block runtime service registrations.freeze_service_registry_after_startup(defaultFalse): freeze this app's scoped service registry after startup.unfreeze_service_registry_on_shutdown(defaultTrue): unfreeze this app's registry when app exits.eager_init_timeout_sec(optional): timeout for eager singleton initialization.
Recommended production defaults:
install_di(
app,
service_packages=["myapp.services"],
strict=True,
freeze_container_after_startup=True,
freeze_service_registry_after_startup=True,
eager_init_timeout_sec=30,
)
Safety and Worker Model
- Container enforces single event-loop usage.
- Container rejects cross-process reuse.
- Registry maps container by current process/thread/event-loop context.
- Runtime service registry is app-scoped, so freeze/unfreeze does not leak across apps.
- Transient finalizers run after request completion.
- Transient finalizers also run after WebSocket connection teardown.
- Singleton teardown runs on shutdown in reverse order.
Supply-Chain Security
Install security tooling group:
uv sync --locked --no-default-groups --group security
Run checks:
./scripts/supply_chain_check.sh
Common Errors
Duplicate service registration for key: same key registered more than once.No service registered for type: missing provider for type-based injection.Multiple services registered for type: use key-based injection instead.Detected circular service dependency: dependency graph has a cycle.Cannot register services after container registrations are frozen: runtime registration attempted after startup.No active app service registry capture: decorated service module was imported beforeinstall_di(...)startup import scanning. Fix:- Move
@Service/@ServiceDictclasses into a dedicated module (for exampledemo/services.py). - Set
install_di(..., service_packages=["demo.services"])to that module path. - Remove early imports of that service module from
main.py.
- Move
Public API
from fastapiex.di import (
AppServiceRegistry,
BaseService,
Inject,
Service,
ServiceDict,
ServiceContainer,
ServiceLifetime,
capture_service_registrations,
install_di,
require,
)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapiex_di-1.0.0.tar.gz.
File metadata
- Download URL: fastapiex_di-1.0.0.tar.gz
- Upload date:
- Size: 88.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f3a51f7446cb8af08f3f16760e0c4ed43ab6b1d207a4075a51ae3d84b85c3d32
|
|
| MD5 |
236abe473f119d310fc5ded0bf335bf4
|
|
| BLAKE2b-256 |
2826a4e268a4a2dec9ea08822e4108a572e6a210b6fcc6c7cac6c3ba07f1c13a
|
File details
Details for the file fastapiex_di-1.0.0-py3-none-any.whl.
File metadata
- Download URL: fastapiex_di-1.0.0-py3-none-any.whl
- Upload date:
- Size: 26.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fa60a4fad251aaecf39d9aff00c6ee2a31181c7bad49539d653377f3274efea
|
|
| MD5 |
a57f3a298dffaa286827f4173bcaca86
|
|
| BLAKE2b-256 |
ef103d5753098cbdb8fa42e4737b273f51b77c3f0eeb855e903c96d8ca7695e9
|