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:

  1. Route Reception: FastAPI matches the GET request in api/routers/notebooks.py and validates query parameters
  2. Service Delegation: The route calls await NotebookService.get_all_notebooks(order_by) to invoke business logic
  3. Data Retrieval: The service queries SurrealDB via repository helpers and constructs Notebook domain objects from open_notebook/domain/notebook.py
  4. Response Serialization: Domain objects transform into NotebookResponse Pydantic models defined in api/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 in api/*_service.py never 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 to api/models.py or open_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 via open_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →