# Three-Tier Architecture in Open Notebook: Routes, Services, and Models Explained

> Discover the three-tier architecture in Open Notebook. Learn how FastAPI routes, services, and models ensure clean code and enhance testability for your projects.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: architecture
- Published: 2026-06-22

---

**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`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/notebooks.py), route handlers accept incoming requests and immediately pass them to service methods:

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/api/notebook_service.py) and [`api/models_service.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/api/notebook_service.py) demonstrates this pattern by processing data before returning domain entities:

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py) define request and response shapes for FastAPI serialization:

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py) exposes helper methods like `save()` to manage SurrealDB persistence:

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py)
4. **Response Serialization**: Domain objects transform into `NotebookResponse` Pydantic models defined in [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/database/repository.py), and translate between API and domain representations
- **Models** exist as Pydantic schemas ([`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py) represent persistent entities with methods like `save()` that interact directly with SurrealDB, while Pydantic schemas in [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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.