Dependency Registration
Registration is the process of telling the container which implementation to provide when a specific interface (protocol) is requested. FastIoC makes this incredibly simple with zero boilerplate.
Creating a Container
First, create a Container instance:
from fastioc import Container
container = Container()
That's it! Your container is ready to register dependencies.
Understanding Interfaces
In FastIoC, any type can be used as an interface - whether it's a Protocol, an abstract class, a regular class, or even a custom marker type. However, Protocols are recommended for class-based dependencies as they provide better type safety, editor support, and enable true interface-based programming following the Dependency Inversion Principle:
from typing import Protocol
class IService(Protocol):
"""This is your interface - what you'll request in endpoints"""
def get_data(self) -> str: ...
Registering Dependencies
FastIoC supports everything that can be a dependency in FastAPI - classes, functions, generators, async generators, callable objects, and more. All of them work seamlessly with both sync and async implementations - FastAPI handles the execution automatically.
Here are the most common patterns:
Class-Based Dependencies
The most common pattern for stateful services:
from typing import Protocol
# 1. Define your interface
class IUserService(Protocol):
def get_user(self, id: int) -> dict: ...
async def get_user_async(self, id: int) -> dict: ...
# 2. Implement the concrete class
class UserService(IUserService):
def __init__(self):
self.db_connection = "postgresql://..."
def get_user(self, id: int) -> dict:
return {"id": id, "name": "John Doe"}
async def get_user_async(self, id: int) -> dict:
# You can mix sync and async methods in the same class
return {"id": id, "name": "Jane Doe"}
# 3. Register with the container
container.add_scoped(IUserService, UserService)
Using the Same Class as Interface:
For simpler projects, you can use the class itself as both the interface and implementation (similar to NestJS):
class UserService:
def get_user(self, id: int) -> dict:
return {"id": id, "name": "John Doe"}
# Register the class to itself
container.add_scoped(UserService, UserService)
However, for larger projects, using separate Protocols as interfaces is considered best practice as it: - Follows the Dependency Inversion Principle (depend on abstractions, not concretions) - Makes your code more testable (easy to mock interfaces) - Provides better separation of concerns - Enables swapping implementations without changing dependent code
Function-Based Dependencies
Perfect for simple, stateless operations or direct value providers:
# 1. Define a marker type for your dependency
class UserId(int):
"""Custom type to identify this dependency"""
...
# 2. Create your function
def get_user_id() -> int:
return 42
# 3. Register with the container
container.add_scoped(UserId, get_user_id)
Async Example:
class ApiKey(str):
...
async def fetch_api_key() -> str:
# FastAPI handles async automatically
return "secret-key-123"
container.add_scoped(ApiKey, fetch_api_key)
Generator-Based Dependencies
Ideal when you need setup and teardown logic (like managing database connections or file handles):
from typing import Generator
class DatabaseConnection(object):
"""Marker type for DB connection"""
...
def get_db_connection() -> Generator[object, None, None]:
# Setup: acquire resource
connection = create_connection()
print("Connection opened")
try:
yield connection # Provide the dependency
finally:
# Teardown: cleanup resource
connection.close()
print("Connection closed")
container.add_scoped(DatabaseConnection, get_db_connection)
Async Generator Example:
from typing import AsyncGenerator
class AsyncDbConnection(object):
...
async def get_async_db() -> AsyncGenerator[object, None]:
# Setup
db = await connect_async()
try:
yield db
finally:
# Cleanup
await db.close()
container.add_scoped(AsyncDbConnection, get_async_db)
Callable Class Instances (Advanced)
For more advanced use cases, you can use callable class instances as dependencies. This pattern is useful when you need to configure a dependency with parameters or maintain state across multiple calls:
# 1. Define a callable class (with __call__ method)
class DatabaseConnectionFactory:
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.call_count = 0
def __call__(self) -> dict:
self.call_count += 1
return {
"connection": self.connection_string,
"call_count": self.call_count
}
# 2. Create an instance with configuration
db_factory = DatabaseConnectionFactory("postgresql://localhost/mydb")
# 3. Define a marker type
class DbConfig(dict):
...
# 4. Register the instance (not the class!)
container.add_scoped(DbConfig, db_factory)
When to use: - When you need to pass configuration to the dependency at registration time - When you need to maintain state across dependency resolutions - When you want more control over the dependency creation process
Note: You're registering the instance (the callable object), not the class itself. Each time the dependency is resolved, the __call__ method is invoked.
@app.get('/db-info')
def get_db_info(config1: DbConfig, config2: DbConfig):
# First call: {"connection": "postgresql://...", "call_count": 1}
# Second call: {"connection": "postgresql://...", "call_count": 2}
# The instance's state is maintained!
return {"config1": config1, "config2": config2}
This pattern gives you the flexibility of functions with the state management of classes.
Registration Methods
FastIoC provides three registration methods that control dependency lifetimes:
container.add_singleton(IService, ServiceImpl) # One instance per application
container.add_scoped(IService, ServiceImpl) # One instance per request
container.add_transient(IService, ServiceImpl) # New instance every time
We'll explore these lifetimes in detail in the Dependency Lifetimes chapter.
Re-registering Dependencies
If you register multiple implementations for the same interface, the last registration always replaces previous ones:
# First registration
container.add_scoped(IUserService, UserServiceV1)
# Second registration - replaces the first one
container.add_scoped(IUserService, UserServiceV2) # This is what will be used
# Third registration - replaces the second one
container.add_scoped(IUserService, UserServiceV3) # This is what will actually be injected
When a dependency is re-registered: - The previous implementation is completely forgotten - Only the last registered implementation is used - No warning or error is raised
When to use this:
This behavior is mainly useful when you accidentally register the same interface twice, or when you intentionally want to replace a registration during initial setup.
When NOT to use this:
β For testing/mocking - Use Dependency Overrides instead (proper way to mock for tests)
β For environment-specific configs - Use Dependency Overrides instead (proper way to handle dev/prod)
Re-registration works, but dependency overrides are the recommended best practice for replacing implementations in different contexts (testing, environments, etc.).
Next Steps
- Learn about Dependency Resolution - how to access registered dependencies
- Understand Dependency Lifetimes - singleton, scoped, and transient behaviors
- Explore Nested Dependencies - automatic resolution of dependency chains