# Frontend-Backend API Communication and Data Fetching Patterns in Open Notebook

> Explore frontend-backend API communication and data fetching patterns in Open Notebook. Learn how FastAPI and TanStack React Query sync data between your Python API and React frontend.

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

---

**Open Notebook uses a FastAPI REST backend and TanStack React Query on the frontend to handle all data fetching, caching, and state synchronization between the React/Next.js UI and the Python API.**

The `lfnovo/open-notebook` repository implements a clean separation of concerns between its frontend and backend layers. All communication flows over HTTP REST endpoints, with the frontend leveraging **TanStack React Query** (formerly React Query) to manage server state, caching, and mutations. This architecture ensures a reactive UI that automatically stays in sync with the FastAPI backend without manual request lifecycle management.

## FastAPI REST Backend Architecture

The backend exposes a RESTful API built on FastAPI, organized into domain-specific routers and centralized in a single application entry point.

### Router Organization

Each domain object has its own dedicated router module under `api/routers/`. For example:

- **[`api/routers/sources.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/sources.py)** – Handles CRUD operations for source records
- **[`api/routers/notebooks.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/notebooks.py)** – Manages notebook creation, listing, and deletion  
- **[`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py)** – Controls chat session management

These routers handle incoming JSON requests and return Pydantic v2 schema objects defined in [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py).

### Central Application Configuration

The routers are assembled into the main FastAPI application in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py). This file also registers essential middleware including CORS handling, password authentication, and a global exception handler:

```python

# api/main.py (conceptual structure)

from fastapi import FastAPI
from routers import notebooks, sources, chat

app = FastAPI()
app.include_router(notebooks.router)
app.include_router(sources.router)
app.include_router(chat.router)

```

### Data Serialization

All request and response payloads use **JSON** with strict validation through **Pydantic v2** schemas. The backend enforces type safety at the API boundary, ensuring the frontend receives consistently shaped data.

## TanStack React Query Frontend

The frontend manages server state through a centralized QueryClient that provides caching, deduplication, and automatic background refetching.

### Query Client Configuration

The QueryClient is instantiated in [`frontend/src/lib/api/query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/query-client.ts) with production-ready defaults:

```typescript
// frontend/src/lib/api/query-client.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,      // 5 minutes
      gcTime: 10 * 60 * 1000,        // 10 minutes garbage collection
      retry: 2,
      refetchOnWindowFocus: false,
    },
    mutations: {
      retry: 1,
    },
  },
});

```

### Typed Query Keys

The same file exports a `QUERY_KEYS` constant that standardizes cache keys across the application:

```typescript
// frontend/src/lib/api/query-client.ts
export const QUERY_KEYS = {
  notebooks: ['notebooks'] as const,
  notebook: (id: string) => ['notebooks', id] as const,
  sources: (notebookId?: string) => ['sources', notebookId] as const,
  // ... additional keys
};

```

### Provider Setup

The entire component tree is wrapped by `QueryProvider` in [`frontend/src/components/providers/QueryProvider.tsx`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/components/providers/QueryProvider.tsx):

```tsx
// frontend/src/components/providers/QueryProvider.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '@/lib/api/query-client';

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

```

## Data Fetching Patterns

Components interact with the backend through standardized hooks that abstract the HTTP layer.

### Read Operations

List and detail views use `useQuery` (or `useInfiniteQuery` for paginated data) combined with the typed query keys:

```tsx
// frontend/src/components/notebooks/NotebookList.tsx
const { data, isLoading } = useQuery({
  queryKey: QUERY_KEYS.notebooks,
  queryFn: () => 
    fetch(`${process.env.NEXT_PUBLIC_API_URL}/notebooks`)
      .then(res => res.json())
});

```

### Write Operations and Cache Invalidation

Mutations follow a consistent pattern in `useMutation` hooks. After a successful mutation, the query cache is invalidated to trigger automatic refetching of dependent components:

```tsx
// frontend/src/components/sources/AddSourceButton.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { QUERY_KEYS } from '@/lib/api/query-client';

export function AddSourceButton({ notebookId }: { notebookId: string }) {
  const queryClient = useQueryClient();

  const addSource = useMutation({
    mutationFn: (payload) => 
      fetch(`${process.env.NEXT_PUBLIC_API_URL}/sources`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      }).then(r => r.json()),
    onSuccess: () => {
      queryClient.invalidateQueries({ 
        queryKey: QUERY_KEYS.sources(notebookId) 
      });
    },
  });

  // Component calls addSource.mutate(payload) on user submission
}

```

### Optimistic UI Updates

Some interactive components (such as chat interfaces) implement **optimistic updates**, immediately modifying the local cache before the server confirms the mutation, then rolling back if the request fails.

## Environment Configuration

The frontend builds API URLs from a single environment variable, making environment switching seamless:

- **Development**: Set `NEXT_PUBLIC_API_URL=http://localhost:5055` in `.env.local`
- **Production**: Point to your deployed API endpoint without modifying component code

This centralization ensures all fetch calls in `fetch(\`${process.env.NEXT_PUBLIC_API_URL}/endpoint\`)` automatically target the correct backend.

## Example Flow: Adding a Source

The following sequence illustrates the complete data fetching pattern when a user adds a new source:

1. **User submits form** → `AddSourceButton` calls `addSource.mutate(payload)`
2. **POST request** → Frontend sends JSON to `POST /sources` (handled by [`api/routers/sources.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/sources.py))
3. **Database operation** → FastAPI creates the record and returns the new object
4. **Cache invalidation** → `onSuccess` callback invalidates `QUERY_KEYS.sources(notebookId)`
5. **Automatic refetch** → React Query refetches the sources list, updating all subscribed components without manual reload

## Summary

- **FastAPI REST backend** in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) exposes domain-specific routers under `api/routers/` with JSON/Pydantic v2 serialization
- **TanStack React Query** manages all server state through a centralized `QueryClient` configured in [`frontend/src/lib/api/query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/query-client.ts)
- **Typed query keys** in `QUERY_KEYS` ensure cache consistency across the application
- **Mutation pattern** combines `useMutation` with `queryClient.invalidateQueries()` to keep UI synchronized after writes
- **Environment-based URL configuration** allows seamless switching between development and production backends

## Frequently Asked Questions

### How does Open Notebook handle cache invalidation after mutations?

When a mutation succeeds, the `onSuccess` callback in the `useMutation` hook calls `queryClient.invalidateQueries()` with the specific query key (e.g., `QUERY_KEYS.sources(notebookId)`). This marks the cached data as stale, triggering React Query to automatically refetch the data and update all components subscribed to that key.

### What caching strategy does the frontend use?

The QueryClient configuration in [`frontend/src/lib/api/query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/query-client.ts) sets a **5-minute stale time** and **10-minute garbage collection time**. Data remains fresh for 5 minutes before background refetching, and unused cache entries are garbage collected after 10 minutes. The configuration also disables refetching on window focus to prevent unnecessary network requests.

### Why does the repository use TanStack React Query instead of fetch or axios directly?

TanStack React Query provides built-in caching, deduplication, error handling, and automatic background refetching that would require significant boilerplate with raw fetch or axios. The library handles complex state synchronization scenarios—such as updating multiple UI components after a mutation—through its centralized cache and query key system.

### How is the API base URL configured across different environments?

The frontend reads `NEXT_PUBLIC_API_URL` from environment variables (typically set in `.env.local` for development). All components construct URLs using `process.env.NEXT_PUBLIC_API_URL`, allowing the same codebase to target `http://localhost:5055` in development or a production domain without code changes.