# How the Open Notebook Next.js Frontend Interacts with the FastAPI Backend and Manages State

> Discover how the Open Notebook Next.js frontend interacts with its FastAPI backend using dynamic configuration, TanStack Query for server-state, and Zustand for UI state.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: deep-dive
- Published: 2026-07-04

---

**The Open Notebook Next.js frontend communicates with its FastAPI backend through a dynamic configuration layer that resolves the API URL at runtime, then uses TanStack Query for server-state caching and Zustand for client-side UI state.**

The `lfnovo/open-notebook` repository implements a clean separation between the Next.js frontend and FastAPI backend. The frontend uses a layered architecture consisting of runtime configuration discovery, thin API wrappers, and dual state management to handle data synchronization and UI responsiveness.

## Dynamic Runtime Configuration

Before making any requests, the frontend must determine where the FastAPI server is running. This is handled dynamically to support different deployment environments without rebuilding the application.

### Resolving the API Base URL

The resolution logic lives in [`frontend/src/lib/config.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/config.ts). When the Next.js app initializes, it calls `getApiUrl()`, which implements a three-tier fallback strategy:

1. **Runtime endpoint**: Calls the internal `/config` route to fetch the API URL dynamically
2. **Build-time variable**: Falls back to `NEXT_PUBLIC_API_URL` environment variable
3. **Relative proxy**: Defaults to an empty string, relying on Next.js rewrite rules to proxy `/api/*` requests

The resolved URL is cached in a module-scoped variable to avoid redundant configuration fetches on subsequent requests.

```typescript
// frontend/src/lib/config.ts
let config: { apiUrl: string } | null = null
let configPromise: Promise<{ apiUrl: string }> | null = null

export async function getApiUrl(): Promise<string> {
  if (config) return config.apiUrl
  
  // Try runtime endpoint first
  const runtimeResp = await fetch('/config', { cache: 'no-store' })
  if (runtimeResp.ok) {
    const { apiUrl } = await runtimeResp.json()
    if (apiUrl) {
      config = { apiUrl }
      return config.apiUrl
    }
  }
  
  // Fallback to build-time env var or relative path
  const finalUrl = process.env.NEXT_PUBLIC_API_URL ?? ''
  config = { apiUrl: finalUrl }
  return config.apiUrl
}

```

## API Client Architecture

With the base URL resolved, the frontend communicates with FastAPI through dedicated wrapper modules that standardize HTTP requests.

### Wrapper Functions for REST Endpoints

Each domain entity has its own API module under `frontend/src/lib/api/`. For example, [`notebooks.ts`](https://github.com/lfnovo/open-notebook/blob/main/notebooks.ts) exports a `notebooksApi` object that constructs URLs using `await getApiUrl()` and executes `fetch` calls. This pattern consolidates request logic and error handling in one location.

```typescript
// frontend/src/lib/api/notebooks.ts
import { getApiUrl } '@/lib/config'

export const notebooksApi = {
  async list(params: { archived?: boolean; order_by?: string }) {
    const base = await getApiUrl()
    const qs = new URLSearchParams(params as Record<string, string>).toString()
    const resp = await fetch(`${base}/api/notebooks?${qs}`)
    if (!resp.ok) throw new Error('Failed to fetch notebooks')
    return resp.json()
  },
  
  async create(data: CreateNotebookRequest) {
    const base = await getApiUrl()
    const resp = await fetch(`${base}/api/notebooks`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    return resp.json()
  }
}

```

## Server-State Management with TanStack Query

The frontend treats FastAPI data as **server state** using TanStack Query (formerly React Query). This library handles caching, background refetching, and synchronization automatically.

### Query Hooks for Data Fetching

Data retrieval hooks like `useNotebooks` in [`frontend/src/lib/hooks/use-notebooks.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/hooks/use-notebooks.ts) wrap the API calls in `useQuery` hooks. They use centralized query keys defined in [`frontend/src/lib/api/query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/query-client.ts) to ensure cache consistency across the application.

```typescript
// frontend/src/lib/hooks/use-notebooks.ts
import { useQuery } from '@tanstack/react-query'
import { notebooksApi } from '@/lib/api/notebooks'
import { QUERY_KEYS } from '@/lib/api/query-client'

export function useNotebooks(archived?: boolean) {
  return useQuery({
    queryKey: [...QUERY_KEYS.notebooks, { archived }],
    queryFn: () => notebooksApi.list({ archived, order_by: 'updated desc' }),
  })
}

```

### Mutation Hooks for Data Modification

Create, update, and delete operations use `useMutation`. On success, these hooks invalidate the relevant query keys, triggering automatic refetches to keep the UI synchronized with the FastAPI backend.

```typescript
// frontend/src/lib/hooks/use-notebooks.ts (continued)
import { useMutation, useQueryClient } from '@tanstack/react-query'

export function useCreateNotebook() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: (data: CreateNotebookRequest) => notebooksApi.create(data),
    onSuccess: () => {
      // Invalidate and refetch the notebooks list
      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.notebooks })
    }
  })
}

```

## Client-Side State Management with Zustand

While TanStack Query manages server data, transient UI state—such as modal visibility, selected items, and theme preferences—lives in a lightweight Zustand store.

### Global UI State Store

The [`frontend/src/lib/store.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/store.ts) file creates a global store using Zustand's `create` function. Components subscribe to this store to read or update UI flags without prop drilling, keeping the component tree clean.

```typescript
// frontend/src/lib/store.ts
import { create } from 'zustand'

interface StoreState {
  isSettingsOpen: boolean
  selectedNotebookId: string | null
  openSettings: () => void
  closeSettings: () => void
  selectNotebook: (id: string | null) => void
}

export const useStore = create<StoreState>((set) => ({
  isSettingsOpen: false,
  selectedNotebookId: null,
  
  openSettings: () => set({ isSettingsOpen: true }),
  closeSettings: () => set({ isSettingsOpen: false }),
  selectNotebook: (id) => set({ selectedNotebookId: id }),
}))

```

This separation ensures that server data (managed by TanStack Query) and UI state (managed by Zustand) remain distinct, preventing synchronization bugs between the frontend and FastAPI backend.

## Summary

- **Runtime configuration** in [`frontend/src/lib/config.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/config.ts) dynamically resolves the FastAPI base URL via the `/config` endpoint, falling back to environment variables.
- **API wrappers** in `frontend/src/lib/api/*.ts` provide typed, reusable `fetch` methods that consume the resolved configuration.
- **TanStack Query** handles server-state caching, background updates, and automatic invalidation when mutations succeed.
- **Zustand** manages client-side UI state like modals and selections, keeping it separate from server data.

## Frequently Asked Questions

### How does the frontend determine the FastAPI backend URL?

The frontend calls `getApiUrl()` from [`frontend/src/lib/config.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/config.ts), which first attempts to fetch from the internal `/config` endpoint. If that fails, it falls back to the `NEXT_PUBLIC_API_URL` environment variable, and finally to an empty string that triggers Next.js rewrite rules.

### What library handles data caching and synchronization?

TanStack Query manages all server-state caching. It stores the results of API calls in a centralized cache, handles background refetching, and invalidates queries automatically when mutations complete, ensuring the UI stays synchronized with the FastAPI backend.

### How is UI state separated from server data?

The application uses **Zustand** for client-side UI state (modal visibility, selected notebook IDs) and **TanStack Query** for server data coming from FastAPI. This architectural split prevents UI interactions from corrupting cached server data and simplifies state logic.

### Where are the API endpoint wrappers defined?

REST API wrappers are located in `frontend/src/lib/api/`, with each file (such as [`notebooks.ts`](https://github.com/lfnovo/open-notebook/blob/main/notebooks.ts)) containing typed functions that build URLs using `getApiUrl()` and execute HTTP requests via `fetch`.