Three-Tier Architecture in Open Notebook: Routes, Services, and Models Explained
Open Notebook implements a strict three-tier architecture that separates FastAPI route handlers, business logic services, and data models to ensure clean boundaries and testability.
The Open Notebook project uses a layered backend design to keep HTTP concerns distinct from business rules and database operations. This three-tier architecture organizes code into routes, services, and models, making the FastAPI application maintainable and easy to test. Understanding this structure is essential for contributors working with the codebase or developers building similar Python applications.
The Three Layers of Open Notebook
The architecture divides responsibilities across three distinct tiers: Routes handle HTTP communication, Services contain business logic, and Models represent data structures.
Routes Layer: FastAPI Endpoints
The routes layer defines HTTP endpoints and handles request parsing, parameter validation, and error responses. Located in api/routers/*.py, these modules delegate all business operations to the service layer and return Pydantic response models.
In api/routers/notebooks.py, route handlers accept incoming requests and immediately pass them to service methods:
# api/routers/notebooks.py
@router.get("/notebooks", response_model=List[NotebookResponse])
async def get_notebooks(order_by: str = Query("updated desc")):
return await notebook_service.get_all_notebooks(order_by)
Routes never contain database logic or raw data manipulation—they serve purely as the HTTP interface layer.
Services Layer: Business Logic Orchestration
Services encapsulate the core business logic and orchestrate interactions between routes and data storage. Implemented in files like api/notebook_service.py and api/models_service.py, these classes translate raw API data into domain objects and manage CRUD operations.
The NotebookService class in api/notebook_service.py demonstrates this pattern by processing data before returning domain entities:
# api/notebook_service.py
class NotebookService:
async def get_all_notebooks(self, order_by: str = "updated desc") -> List[Notebook]:
notebooks_data = api_client.get_notebooks(order_by=order_by)
notebooks = []
for nb_data in notebooks_data:
nb = Notebook(
name=nb_data["name"],
description=nb_data["description"],
archived=nb_data["archived"],
)
nb.id = nb_data["id"]
notebooks.append(nb)
return notebooks
Services utilize the database helper repo_query from open_notebook/database/repository.py to execute SurrealDB operations while keeping storage implementation details isolated from the HTTP layer.
Models Layer: Data Representation
Open Notebook uses two distinct model types to separate API contracts from persistent entities.
Pydantic Schemas in api/models.py define request and response shapes for FastAPI serialization:
# api/models.py
class NotebookResponse(BaseModel):
id: str
name: str
description: str
archived: bool
created: str
updated: str
source_count: int
note_count: int
Domain Entities in open_notebook/domain/*.py represent persistent objects and encapsulate database interactions. The Notebook class in open_notebook/domain/notebook.py exposes helper methods like save() to manage SurrealDB persistence:
# open_notebook/domain/notebook.py
class Notebook:
def __init__(self, name: str, description: str, archived: bool = False):
self.id: Optional[str] = None
self.name = name
self.description = description
self.archived = archived
self.created = None
self.updated = None
async def save(self):
# Persist via SurrealDB repository
await repo_save(self) # simplified illustration
Request Lifecycle: Data Flow Through the Tiers
When a client requests /notebooks, the three-tier architecture processes the request through a strict pipeline:
- Route Reception: FastAPI matches the GET request in
api/routers/notebooks.pyand validates query parameters - Service Delegation: The route calls
await NotebookService.get_all_notebooks(order_by)to invoke business logic - Data Retrieval: The service queries SurrealDB via repository helpers and constructs
Notebookdomain objects fromopen_notebook/domain/notebook.py - Response Serialization: Domain objects transform into
NotebookResponsePydantic models defined inapi/models.py, which FastAPI serializes to JSON
This flow ensures that HTTP handling, business rules, and data persistence remain decoupled.
Benefits of the Three-Tier Design
This architectural pattern enforces several critical software engineering principles:
- Clear Boundaries: Routing code in
api/routers/never contains database logic, while services inapi/*_service.pynever manipulate raw HTTP request objects - Testability: Services can be unit-tested with mock domain models, and routes can be exercised using FastAPI’s TestClient without requiring live database connections
- Maintainability: Adding new endpoints requires only a router stub in
api/routers/, a corresponding service method, and optional additions toapi/models.pyoropen_notebook/domain/
Summary
- Routes (
api/routers/*.py) handle HTTP parsing and validation, delegating immediately to service methods - Services (
api/*_service.py) contain business logic, orchestrate database calls viaopen_notebook/database/repository.py, and translate between API and domain representations - Models exist as Pydantic schemas (
api/models.py) for HTTP serialization and domain entities (open_notebook/domain/) for database persistence - This separation enables independent testing of business logic and simplifies maintenance across the Open Notebook codebase
Frequently Asked Questions
What is the purpose of the services layer in Open Notebook?
The services layer acts as an intermediary that keeps route handlers thin and database interactions isolated. By placing business logic in classes like NotebookService within api/notebook_service.py, the codebase ensures that HTTP concerns in routers remain separate from data manipulation, allowing business rules to be tested independently of the web framework.
How do domain models differ from Pydantic schemas in this architecture?
Domain models in open_notebook/domain/notebook.py represent persistent entities with methods like save() that interact directly with SurrealDB, while Pydantic schemas in api/models.py define the JSON shapes for API requests and responses. Controllers receive JSON validated against Pydantic schemas, then services convert this data into domain entities for database operations.
Where is the database logic located in Open Notebook's three-tier structure?
Database access logic resides primarily in open_notebook/database/repository.py, which provides low-level helpers like repo_query used by the service layer. Domain entities in open_notebook/domain/ may also encapsulate persistence methods, but no database code exists in the route handlers located in api/routers/.
Can the service layer be tested independently of FastAPI routes?
Yes, the service layer is designed for isolated unit testing. Because services depend on domain models rather than HTTP request objects, developers can mock the Notebook class and database repository methods to test business logic without instantiating FastAPI applications or making actual database connections.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →