FastAPI Dependency Injection
This pattern combines the Ports and Adapters idea with FastAPI’s dependency
injection API. In this example, I’m passing data from a “frontend”, a dead
simple index.html
file, through FastAPI, to a MongoDB instance. I want to
separate my data source from by logic as much as possible, within reason, so
I’m using the Ports and Adapters architecture for setting up my MongoDB
database. I’ll collect all of my adapters in the adapters directory, since
MongoDB is a database, I’m putting it into an adapters.database
module. The
idea is to have a DatabaseInterface
, which is the “port” in this framework,
and a MongoAdapter
that implements the DatabaseInterface
. From FastAPI’s
perspective, it’s using the DataBaseInterface
in the application code, except
for a block at the top of main.py
that configures the MongoAdapter
.
Project Organization
This is the directory structure.
project/
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── src/
│ │ ├── __init__.py
│ │ ├── adapters/
│ │ │ ├── __init__.py
│ │ │ └── database.py
│ │ └── main.py
│ ├── test/
│ │ └── __init__.py
│ └── venv/
├── .gitignore
├── README.markdown
├── docker-compose.yaml
└── index.html
Docker Configuration
The docker compose
configuration is minimal, just a web
container for
FastAPI, and a db
container for MongoDB.
services:
web:
build: ./backend
ports:
- "8000:8000"
depends_on:
- db
environment:
- MONGO_URI=mongodb://db:27017
db:
image: mongo:latest
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:
The Dockerfile responsible for the FastAPI server is also minimal.
FROM python:3.13
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY src/ .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
For completeness, this is the requirements.txt
.
annotated-types==0.7.0
anyio==4.8.0
click==8.1.8
dnspython==2.7.0
fastapi==0.115.7
h11==0.14.0
httptools==0.6.4
idna==3.10
pydantic==2.10.5
pydantic_core==2.27.2
pymongo==4.10.1
python-dotenv==1.0.1
PyYAML==6.0.2
sniffio==1.3.1
starlette==0.45.2
typing_extensions==4.12.2
uvicorn==0.34.0
uvloop==0.21.0
watchfiles==1.0.4
websockets==14.2
FastAPI
This is the basic idea for the server. I use the MongoAdapter
at the top of
file, and then the rest of the routes use a db: DatabaseDependency
, unaware
of any other details. FastAPI’s dependency injection API prefers that users use
the typing.Annotated
type to wrap the type, in our case DatabaseInterface
,
and the metadata, the mongo_dependency()
function wrapped in FastAPI’s
Depends
object.
In this example, we’re communicating with the client over websockets, and then
writing to mongo through the DatabaseDependency
abstraction.
# built-in imports
import json
import logging
from typing import Annotated
import os
# in-house imports
from adapters.database import DatabaseInterface, MongoAdapter
# 3rd-party imports
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Depends
app = FastAPI()
# logging configuration
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# db configuration
MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017')
async def mongo_dependency():
db = MongoAdapter(MONGO_URI, "database", "items")
try:
yield db
finally:
db.close()
DatabaseDependency = Annotated[DatabaseInterface, Depends(mongo_dependency)]
# routes
@app.get("/")
async def root():
return {"message": "hello"}
@app.post("/items")
async def create_item(item: dict, db: DatabaseDependency):
try:
result = db.create(item)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating item: {e}")
@app.get("/items")
async def read_items(db: DatabaseDependency):
try:
items = db.read()
return items
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting items: {e}")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, db: DatabaseDependency):
await websocket.accept() # Accept the connection
logger.info(">>> Client connected")
try:
while True:
data = await websocket.receive_text() # Receive data from the client
data = json.loads(data)
logger.info(f">>> Received from client: {data}")
item = {data.get("key"): data.get("value")}
try:
result = db.create(item)
await websocket.send_text(f"Message received: {result}")
except Exception as e:
logger.error(f"Error creating item: {e}")
except WebSocketDisconnect:
logger.info(">>> Client disconnected")
Finally, in the src/adapters/database.py
module I have this. The
DatabaseInterface
defines the shape of what the client code can expect, and
the MongoAdapter
implements that interface. When it is time to change the
database, then we would write another class implementing the interface.
from pymongo import MongoClient
class DatabaseInterface:
def create(self, item: dict):
pass
def read(self):
pass
def close(self):
pass
class MongoAdapter(DatabaseInterface):
def __init__(self, uri: str, database: str, collection: str):
try:
self.client = MongoClient(uri)
self.db = self.client[database]
self.collection = self.db[collection]
except Exception as e:
self.close()
raise Exception(f"Error connecting to MongoDB: {e}")
def create(self, item: dict):
try:
result = self.collection.insert_one(item)
item["_id"] = str(result.inserted_id)
return item
except Exception as e:
raise Exception(f"Error creating item: {e}")
def read(self):
try:
items = list(self.collection.find({},{"_id":0})) # exclude the mongo _id
for item in items:
if "_id" in item:
item["_id"] = str(item["_id"]) # Convert ObjectId to string
return items
except Exception as e:
raise Exception(f"Error getting items: {e}")
def close(self):
self.client.close()
The Client
All this does is allow a user to submit a key value pair over a websocket, and then see the result that comes back from the server.
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Example</title>
</head>
<body>
<h1>WebSocket Test</h1>
<input type="text" id="keyInput" placeholder="Enter key">
<input type="text" id="valueInput" placeholder="Enter value">
<button onclick="sendMessage()">Send</button>
<div id="messages"></div>
<script>
const websocket = new WebSocket("ws://localhost:8000/ws"); // Replace with your server URL
websocket.onopen = (event) => {
console.log("WebSocket connection opened:", event);
displayMessage("Connected to server!");
};
websocket.onmessage = (event) => {
console.log("Received from server:", event.data);
displayMessage("Server: " + event.data);
};
websocket.onclose = (event) => {
console.log("WebSocket connection closed:", event);
displayMessage("Disconnected from server!");
};
websocket.onerror = (error) => {
console.error("WebSocket error:", error);
displayMessage("Error: " + error);
};
function sendMessage() {
const keyInput = document.getElementById("keyInput");
const valueInput = document.getElementById("valueInput");
if (keyInput.value && valueInput.value) {
const packet = {
key: keyInput.value,
value: valueInput.value
}
websocket.send(JSON.stringify(packet));
displayMessage(JSON.stringify(packet));
messageInput.value = "";
}
}
function displayMessage(message) {
const messagesDiv = document.getElementById("messages");
const messageElement = document.createElement("p");
messageElement.textContent = message;
messagesDiv.appendChild(messageElement);
}
</script>
</body>
</html>