# How to Add New API Endpoints in FastAPI: The Open Notebook Service Pattern

> Learn the service pattern for adding API endpoints in FastAPI: routers, services, and models. Separate HTTP concerns from business logic with Open Notebook's clear architecture.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-28

---

**Open Notebook implements a strict three-layer architecture—routers, services, and models—to keep HTTP concerns separate from business logic, with all endpoints registered centrally in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) using `app.include_router()`.**

The open-notebook repository organizes its FastAPI backend using a consistent service pattern that promotes testability and separation of concerns. Rather than embedding database queries or AI processing directly in route handlers, the codebase delegates all business logic to dedicated service modules. This guide walks through the exact implementation steps found in the source code, from creating routers to wiring everything together in the main application factory.

## The Three-Layer Architecture

Open Notebook structures every API feature across three distinct layers. **Routers** in `api/routers/` handle HTTP request/response cycles and validation. **Services** contain async business logic functions that interact with SurrealDB, LangGraph, or external APIs. **Models** in [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py) define Pydantic schemas for type safety. This separation ensures that [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) remains a thin orchestration layer that merely imports and registers router instances after applying CORS and authentication middleware.

## Step-by-Step Implementation Guide

### 1. Create the Router in `api/routers/`

Start by defining an `APIRouter` instance in a new file within `api/routers/`. Path operation functions should import request/response schemas from [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py) and delegate execution to service functions. The existing [`api/routers/notes.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/notes.py) demonstrates this pattern by keeping route handlers focused on HTTP status codes and validation while forwarding validated payloads to service layers.

Key requirements for router implementation:
- Use `APIRouter()` to define the router instance
- Import Pydantic models from [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py) for `response_model` and request bodies
- Call async service functions rather than embedding business logic
- Handle exceptions with `HTTPException` for proper status code propagation

### 2. Implement the Service Layer

Create a dedicated service file (e.g., [`api/my_feature_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/my_feature_service.py)) containing async functions that perform the actual work. These functions query the database via `repo_query` from `open_notebook.database.repository`, invoke LangGraph graphs, or call external AI APIs. The [`api/sources_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/sources_service.py) file provides a reference implementation showing how to keep database queries and business rules isolated from HTTP transport concerns.

Service functions should accept Pydantic models as parameters and return dictionaries that match response schemas. This allows routers to remain agnostic about implementation details while ensuring type safety at the boundaries.

### 3. Define Pydantic Schemas in [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py)

Add request and response models to [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py) or a dedicated models file. Use `Field` descriptions, validators, and `Literal` constraints as shown in the existing `NoteCreate`, `NoteResponse`, and `NoteUpdate` classes. These schemas enforce validation at the HTTP layer before data reaches service functions, reducing the need for defensive coding in business logic.

### 4. Register the Router in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py)

In [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py), import the new router module at the top of the file alongside existing routers. Register it using `app.include_router()` after the CORS and authentication middleware setup. This ensures the new endpoint inherits security behaviors and appears in the OpenAPI documentation. The registration follows the pattern:

```python
from api.routers import my_feature
app.include_router(my_feature.router, tags=["my_feature"])

```

According to the documentation in [`api/CLAUDE.md`](https://github.com/lfnovo/open-notebook/blob/main/api/CLAUDE.md) (lines 35-43), this central registration approach maintains consistent middleware application across all endpoints.

## Complete Working Example

The following files demonstrate the full service pattern for a hypothetical `/my-feature` endpoint:

**Router ([`api/routers/my_feature.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/my_feature.py)):**

```python
from fastapi import APIRouter, HTTPException
from api.models import MyFeatureRequest, MyFeatureResponse
from api.my_feature_service import run_my_feature

router = APIRouter()

@router.post("/my-feature", response_model=MyFeatureResponse)
async def my_feature_endpoint(payload: MyFeatureRequest):
    try:
        result = await run_my_feature(payload)
        return MyFeatureResponse(**result)
    except Exception as exc:
        raise HTTPException(status_code=400, detail=str(exc))

```

**Service ([`api/my_feature_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/my_feature_service.py)):**

```python
from open_notebook.database.repository import repo_query
from api.models import MyFeatureRequest

async def run_my_feature(req: MyFeatureRequest) -> dict:
    # Query SurrealDB, invoke LangGraph graphs, or call external APIs

    data = await repo_query("SELECT * FROM my_data WHERE id = $id", {"id": req.id})
    # Business logic processing...

    return {"result": data, "status": "ok"}

```

**Models ([`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py)):**

```python
from pydantic import BaseModel, Field

class MyFeatureRequest(BaseModel):
    id: str = Field(..., description="Identifier of the resource")

class MyFeatureResponse(BaseModel):
    result: dict
    status: str

```

**Registration ([`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py)):**

```python
from api.routers import my_feature

# After middleware setup...

app.include_router(my_feature.router, tags=["my_feature"])

```

## Testing and Verification

Launch the application using `uv run uvicorn api.main:app --host 0.0.0.0 --port 5055` and navigate to `http://localhost:5055/docs` to verify the endpoint appears in the OpenAPI schema. For unit testing, import service functions directly to bypass HTTP layers, or use FastAPI's `TestClient` to exercise the full router stack while mocking database calls.

## Summary

- **Open Notebook** uses a three-layer architecture: routers handle HTTP, services contain business logic, and models define schemas
- **Routers** reside in `api/routers/` and use `APIRouter` with Pydantic response models
- **Services** are standalone async functions that interact with SurrealDB via `repo_query` and manage AI processing
- **Registration** happens exclusively in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) using `app.include_router()` after middleware configuration
- **Reference implementations** exist in [`api/routers/notes.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/notes.py) and [`api/sources_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/sources_service.py)

## Frequently Asked Questions

### Where should I place business logic when adding a new FastAPI endpoint?

Place business logic in a dedicated service file within the `api/` directory (e.g., [`api/my_feature_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/my_feature_service.py)). This keeps your router thin and focused on HTTP concerns while making the business logic testable without HTTP dependencies. The [`api/sources_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/sources_service.py) file demonstrates this pattern with database queries separated from route handling.

### How does Open Notebook handle middleware and authentication for new endpoints?

New endpoints automatically inherit CORS and authentication middleware because all routers are registered in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) after the middleware stack is configured. When you call `app.include_router()` in the main application file, FastAPI applies the existing middleware chain to every route in that router, ensuring consistent security and cross-origin handling.

### Can I use separate model files instead of [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py)?

Yes, while [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py) serves as the central schema repository, you can create dedicated model files like [`api/my_feature_models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/my_feature_models.py) for complex features. Import these schemas into your router and service files as needed. The critical requirement is that request/response validation uses Pydantic BaseModel classes with proper Field definitions and type hints.

### What is the recommended way to test new endpoints in this architecture?

Test the service layer directly by importing async functions from your service module and mocking the `repo_query` calls or external API clients. For integration testing, use FastAPI's `TestClient` to hit the router endpoints while mocking the service layer. This approach validates both the HTTP contract and business logic independently, following the separation of concerns established in the codebase.