How to Extend the FastAPI Backend with New Endpoints and Services in Open Notebook
To extend the Open Notebook FastAPI backend, create an async service module in api/ for business logic, define an APIRouter in api/routers/ for HTTP handling, and register the router in api/main.py using app.include_router() with the /api prefix.
Open Notebook is an open-source notebook application built on FastAPI that enforces a clean separation between business logic and HTTP concerns. Extending the FastAPI backend with new endpoints requires following the established pattern of service layers and router modules used throughout the lfnovo/open-notebook repository. This approach ensures that new functionality automatically inherits the existing authentication middleware, CORS handling, and error management configured in the central application.
Architecture Overview
The backend follows a three-tier pattern that keeps the codebase maintainable and testable. At the entry point, [api/main.py](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) initializes the FastAPI application, configures the PasswordAuthMiddleware for authentication, sets up CORS handling, and registers all routers with app.include_router().
Service layers contain the core business logic, database queries, and validation rules. These are plain Python modules (like [api/notebook_service.py](https://github.com/lfnovo/open-notebook/blob/main/api/notebook_service.py)) that expose async functions and raise custom exceptions defined in [open_notebook/exceptions.py](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/exceptions.py).
Router modules handle the HTTP interface, converting incoming requests into service calls and formatting responses. They use FastAPI's APIRouter class (as seen in [api/routers/notebooks.py](https://github.com/lfnovo/open-notebook/blob/main/api/routers/notebooks.py)) and automatically benefit from the middleware stack attached in api/main.py.
Step-by-Step Implementation
1. Create the Service Layer
Create a new service file in the api/ directory alongside existing services. This module should contain async functions that perform the core work, validate inputs, and raise custom exceptions when business rules are violated.
# api/my_feature_service.py
from open_notebook.exceptions import NotFoundError, InvalidInputError
from open_notebook.database.repository import repo_query
async def get_status() -> dict[str, str]:
"""Core business logic for checking system status."""
return {"status": "ready"}
async def echo_message(message: str) -> dict[str, str]:
"""Validate input and return echo."""
if not message:
raise InvalidInputError("Message cannot be empty")
return {"echo": message}
Services should remain agnostic to HTTP concerns. They interact with the database through the repository pattern and use the custom exception hierarchy (such as InvalidInputError) to signal failures.
2. Define the Router
Create a new router file in api/routers/ that imports your service, defines Pydantic request/response models, and maps HTTP methods to service functions. Raise HTTPException to translate service exceptions into proper HTTP status codes.
# api/routers/my_feature.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from api.my_feature_service import get_status, echo_message
router = APIRouter()
class EchoRequest(BaseModel):
message: str
class EchoResponse(BaseModel):
echo: str
@router.get("/my-feature/status", response_model=dict)
async def status():
"""GET endpoint for status check."""
return await get_status()
@router.post("/my-feature/echo", response_model=EchoResponse)
async def echo(req: EchoRequest):
"""POST endpoint for echo service."""
try:
result = await echo_message(req.message)
return EchoResponse(**result)
except InvalidInputError as e:
raise HTTPException(status_code=400, detail=str(e))
Routers should handle request validation through Pydantic models and catch service exceptions to convert them to appropriate HTTP responses (400 for validation errors, 404 for missing resources, etc.).
3. Register the Router in the Application
Import your new router module in [api/main.py](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) and register it using app.include_router(). Place the registration alongside existing router includes to maintain consistency.
# api/main.py
from fastapi import FastAPI
from api.routers import notebooks, my_feature
app = FastAPI()
# Existing middleware configuration (CORS, PasswordAuthMiddleware)...
# Register routers
app.include_router(notebooks.router, prefix="/api", tags=["notebooks"])
app.include_router(my_feature.router, prefix="/api", tags=["my-feature"])
The prefix="/api" parameter ensures all endpoints follow the existing URL structure, while tags organizes endpoints in the automatic API documentation. Because middleware is attached to the app instance before router registration, your new endpoints automatically enforce authentication and CORS policies.
Complete Working Example
Here is the full implementation of a custom feature extending the Open Notebook backend:
Service (api/my_feature_service.py):
from open_notebook.exceptions import InvalidInputError
async def get_status() -> dict[str, str]:
return {"status": "ready", "service": "my-feature"}
async def echo_message(message: str) -> dict[str, str]:
if not message or len(message) > 1000:
raise InvalidInputError("Message must be between 1 and 1000 characters")
return {"echo": message, "length": len(message)}
Router (api/routers/my_feature.py):
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from api.my_feature_service import get_status, echo_message
router = APIRouter()
class EchoRequest(BaseModel):
message: str
class EchoResponse(BaseModel):
echo: str
length: int
@router.get("/my-feature/status")
async def status():
return await get_status()
@router.post("/my-feature/echo", response_model=EchoResponse)
async def echo(req: EchoRequest):
try:
return await echo_message(req.message)
except InvalidInputError as e:
raise HTTPException(status_code=400, detail=str(e))
Registration (api/main.py):
from api.routers import my_feature
# ... existing app initialization ...
app.include_router(my_feature.router, prefix="/api", tags=["my-feature"])
Key Files Reference
- [
api/main.py](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) – FastAPI application factory, middleware configuration (PasswordAuthMiddleware, CORS), and router registration central hub. - [
api/notebook_service.py](https://github.com/lfnovo/open-notebook/blob/main/api/notebook_service.py) – Reference implementation of the service layer pattern showing database access and business logic separation. - [
api/routers/notebooks.py](https://github.com/lfnovo/open-notebook/blob/main/api/routers/notebooks.py) – Production example of a router handling HTTP requests, validation, and error mapping. - [
open_notebook/exceptions.py](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/exceptions.py) – Custom exception hierarchy (InvalidInputError,NotFoundError) used for error handling across services.
Summary
- Service-first architecture: Place business logic in async functions within
api/service modules, keeping them independent of HTTP concerns. - Router registration: Import and register new routers in
api/main.pyusingapp.include_router(router, prefix="/api", tags=["tag-name"])to maintain consistent URL structure. - Automatic middleware: New endpoints automatically inherit
PasswordAuthMiddlewareand CORS handling because they are attached to the FastAPI app instance before router registration. - Error handling: Raise custom exceptions in services (from
open_notebook/exceptions.py) and catch them in routers to convert to appropriateHTTPExceptionresponses. - Pydantic validation: Use Pydantic models for request/response schemas to leverage FastAPI's automatic validation and OpenAPI documentation generation.
Frequently Asked Questions
Do new endpoints automatically require authentication?
Yes. The PasswordAuthMiddleware is configured in api/main.py before routers are registered, so all endpoints—including newly added ones—automatically enforce authentication unless explicitly excluded from the middleware configuration.
How should I handle errors in new services?
Raise custom exceptions defined in open_notebook/exceptions.py (such as InvalidInputError or NotFoundError) from your service functions. Catch these exceptions in your router and convert them to FastAPI's HTTPException with appropriate status codes (400 for validation errors, 404 for missing resources).
Where should I write tests for new endpoints?
Test files are located under the tests/ directory. Follow the existing test patterns to cover both the service layer (business logic and database interactions) and the router layer (HTTP request handling and validation). Ensure tests verify that your endpoints correctly respond to authenticated requests and handle error cases.
Can I change the API prefix for specific endpoints?
While you can customize the prefix parameter in app.include_router(), maintain consistency with the existing /api prefix used throughout the Open Notebook backend. If you need nested routing, define path prefixes in the router itself (e.g., @router.get("/my-feature/sub-resource")) rather than changing the global prefix in main.py.
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 →