In the previous article, we set up a decoupled Python backend using FastAPI. However, our controller logic was still somewhat coupled to the API layer. Today, we’re going to introduce Dependency Injection to fully separate our business logic from the web framework.
In Python, we have a powerful DI libraries, but given the nature of the application and the decoupled architecture, we will use: dependency-injector.
Why Dependency Injection?
Dependency Injection allows us to:
- Decouple components: Our business logic doesn’t need to know how its dependencies are created.
- Improve testability: We can easily mock dependencies (like databases or external APIs) during testing.
- Centralize configuration: All wiring happens in one place (the Container).
While FastAPI has its own built-in DI system (Depends), using a framework-agnostic library like dependency-injector ensures our core business logic remains independent of FastAPI itself.
Installing dependency-injector
First, let’s add the library using uv:
uv add dependency-injectorCreating the Core Layer
We want our business logic to live in src/core, completely unaware of FastAPI.
1. The Core Controller
Let’s create a pure Python controller that handles our business logic.
from .note_dto import NoteDto
class NoteController:
def get_notes(self) -> list[NoteDto]: return [ NoteDto(id=1, content="Hello 1 - from core controller"), NoteDto(id=2, content="Hello 2 - from core controller"), ]2. The DI Container
Now, let’s create the container that will manage our dependencies. This is the Python equivalent of the Awilix container.
from dependency_injector import containers, providersfrom src.core.features.note.note_controller import NoteController
class Container(containers.DeclarativeContainer): note_controller = providers.Singleton(NoteController)
container = Container()The NoteDto should be moved from src/apps/rest_api/frameworks/fastapi/rest_controllers/note_dto.py to src/core/features/note/note_dto.py, since DTOs belong in the core layer where the business logic resides.
Wiring it into FastAPI
Now we need to connect our framework-agnostic core to our FastAPI application. We’ll update our NoteRestController to inject the controller from the container.
from fastapi import APIRouterfrom .note_dto import NoteDtofrom src.core.features.note.note_dto import NoteDtofrom src.core.features.note.note_controller import NoteControllerfrom src.core.container.container import container
class NoteRestController: def __init__(self) -> None: self.note_controller: NoteController = container.note_controller()
self.router = APIRouter(prefix="/notes", tags=["notes"]) self._setup_routes()
def _setup_routes(self) -> None: self.router.add_api_route( "/", self.get_notes, methods=["GET"], response_model=list[NoteDto], )
async def get_notes(self) -> list[NoteDto]: return [ NoteDto(id=1, content="Hello 1"), NoteDto(id=2, content="Hello 2"), ] return self.note_controller.get_notes()
note_controller = NoteRestController()router = note_controller.routerThe Result
Now, when a request comes in:
- FastAPI receives the request at
/notes. NoteRestController(the Adapter) receives the call.- It delegates to
self.note_controller(the Core Logic). NoteControllerreturns the data.
We have successfully achieved Clean Architecture! Our core logic is pure Python, and FastAPI is just a delivery mechanism.
Conclusion
By using dependency-injector, we’ve replicated the flexible, decoupled architecture of our Node.js application in Python. We’re not just writing “scripts”; we’re building a robust, maintainable software system.
In the next article, we’ll look at adding persistence with a database, maintaining this strict separation of concerns.