How the Open Notebook Next.js Frontend Interacts with the FastAPI Backend and Manages State
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. When the Next.js app initializes, it calls getApiUrl(), which implements a three-tier fallback strategy:
- Runtime endpoint: Calls the internal
/configroute to fetch the API URL dynamically - Build-time variable: Falls back to
NEXT_PUBLIC_API_URLenvironment variable - 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.
// 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 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.
// 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 wrap the API calls in useQuery hooks. They use centralized query keys defined in frontend/src/lib/api/query-client.ts to ensure cache consistency across the application.
// 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.
// 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 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.
// 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.tsdynamically resolves the FastAPI base URL via the/configendpoint, falling back to environment variables. - API wrappers in
frontend/src/lib/api/*.tsprovide typed, reusablefetchmethods 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, 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) containing typed functions that build URLs using getApiUrl() and execute HTTP requests via fetch.
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 →