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

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 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 define Pydantic schemas for type safety. This separation ensures that 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 and delegate execution to service functions. The existing 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 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) 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 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

Add request and response models to 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

In 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:

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

According to the documentation in 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):

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):

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):

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):

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 using app.include_router() after middleware configuration
  • Reference implementations exist in api/routers/notes.py and 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). This keeps your router thin and focused on HTTP concerns while making the business logic testable without HTTP dependencies. The 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 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?

Yes, while api/models.py serves as the central schema repository, you can create dedicated model files like 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.

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.

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 →