Frontend Integration with REST API using TanStack Query and Zustand in Open-Notebook
Open-Notebook's Next.js 16 frontend communicates with its FastAPI backend through a REST API layer managed by TanStack Query for server state and Zustand for client-side UI state.
The open-notebook repository (lfnovo/open-notebook) implements a TypeScript React architecture that strictly separates remote data synchronization from local interface preferences. This guide examines how the frontend leverages TanStack Query for declarative REST API interactions while using Zustand for lightweight, persistent UI state management.
Architecture Overview
The integration follows a clean separation of concerns: TanStack Query (@tanstack/react-query) handles all server-derived state—fetching, caching, and mutating data from the FastAPI backend—while Zustand manages client-only state like themes and view modes. This prevents accidental cache corruption and ensures UI components remain responsive during long-running LLM operations.
The data flow follows this path: REST endpoints → generic Axios client → domain API modules → TanStack Query hooks → React components. Zustand stores exist alongside this pipeline, providing immediate access to user preferences without network calls.
The API Client Layer
Axios Configuration with Dynamic Base URL
At the foundation lies frontend/src/lib/api/client.ts, a thin Axios wrapper that configures runtime parameters. It injects the base URL dynamically and attaches authentication tokens from the persisted auth-storage store. Critically, it sets a 10-minute timeout to accommodate long-running LLM calls common in notebook operations.
// Conceptual representation of the client configuration
import axios from 'axios';
export const apiClient = axios.create({
timeout: 600000, // 10 minutes for LLM operations
headers: {
Authorization: `Bearer ${getAuthToken()}`
}
});
Domain-Specific API Modules
Each backend resource maps to a dedicated service module. For example, frontend/src/lib/api/notebooks.ts forwards calls to the generic client, providing typed methods for notebook operations. These modules remain thin, containing only HTTP logic and response typing, leaving state management to the hook layer.
Managing Server State with TanStack Query
Query Hooks and Cache Keys
The frontend/src/lib/hooks/use-notebooks.ts file defines the data fetching strategy. It exports useNotebooks, which wraps useQuery with a structured key system (QUERY_KEYS.notebooks) and parameterized fetching based on the archived flag.
// From frontend/src/lib/hooks/use-notebooks.ts
import { useQuery } '@tanstack/react-query';
export function useNotebooks(archived: boolean = false) {
return useQuery({
queryKey: [QUERY_KEYS.notebooks, { archived }],
queryFn: () => fetchNotebooks({ archived }),
});
}
Mutations and Automatic Cache Invalidation
Create, update, and delete operations use useMutation hooks that automatically invalidate stale cache entries. When useCreateNotebook succeeds, it triggers invalidation of both the list key (QUERY_KEYS.notebooks) and specific entity keys (QUERY_KEYS.notebook(id)), ensuring UI components display fresh data without manual refresh.
// From frontend/src/lib/hooks/use-notebooks.ts (lines 24-38)
export function useCreateNotebook() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createNotebookApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.notebooks] });
},
onError: (error) => {
showToast(getApiErrorKey(error));
}
});
}
Managing Client State with Zustand
Persistent Theme Management
UI-wide preferences live in frontend/src/lib/stores/theme-store.ts. The store uses Zustand's persist middleware to save the user's theme selection (light/dark/system) to localStorage, surviving page reloads. The setTheme method also updates the <html> element's class list to apply Tailwind dark mode classes immediately.
// From frontend/src/lib/stores/theme-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useThemeStore = create(
persist(
(set) => ({
theme: 'system',
setTheme: (theme) => {
set({ theme });
document.documentElement.classList.toggle('dark', theme === 'dark');
},
isDark: () => theme === 'dark' || (theme === 'system' && systemPrefersDark())
}),
{ name: 'theme-storage' }
)
);
Notebook View Preferences
Similarly, frontend/src/lib/stores/notebook-view-store.ts manages the notebook display mode (tile vs. list layout). This state changes frequently during user interaction but never requires API synchronization, making it ideal for Zustand's client-side storage.
Error Handling and UI Feedback
The frontend/src/lib/hooks/use-toast.ts utility provides consistent notification UI across all mutations. Each mutation's onError handler extracts human-readable error keys using getApiErrorKey and dispatches them through the toast system, ensuring uniform error presentation without component-level boilerplate.
Code Examples in Practice
Fetch notebooks with loading states and automatic background updates:
import { useNotebooks } '@/lib/hooks/use-notebooks';
import { LoadingSpinner } '@/components/common/LoadingSpinner';
export default function NotebookList({ showArchived }: { showArchived?: boolean }) {
const { data: notebooks = [], isLoading, isError } = useNotebooks(showArchived ?? false);
if (isLoading) return <LoadingSpinner />;
if (isError) return <p>Failed to load notebooks.</p>;
return (
<ul>
{notebooks.map(nb => (
<li key={nb.id}>{nb.title}</li>
))}
</ul>
);
}
Create a notebook and automatically refresh the list:
import { useCreateNotebook } '@/lib/hooks/use-notebooks';
import { useState } from 'react';
export function NewNotebookForm() {
const [title, setTitle] = useState('');
const createNotebook = useCreateNotebook();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await createNotebook.mutateAsync({ title });
setTitle('');
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Notebook title"
/>
<button type="submit" disabled={createNotebook.isLoading}>
Create
</button>
</form>
);
}
Toggle themes with persistent storage:
import { useTheme } '@/lib/stores/theme-store';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Switch to {theme === 'dark' ? 'Light' : 'Dark'} Mode
</button>
);
}
Summary
- TanStack Query in
frontend/src/lib/hooks/use-notebooks.tsmanages all server state, providing automatic caching, background refetching, and optimistic updates for REST API resources. - Zustand stores in
frontend/src/lib/stores/theme-store.tsandnotebook-view-store.tshandle client-only UI state withpersistmiddleware forlocalStoragedurability. - The API client at
frontend/src/lib/api/client.tsconfigures a 10-minute timeout and dynamic auth injection to support long-running LLM operations. - Cache invalidation happens automatically after mutations, ensuring UI consistency without manual state management.
- Error handling flows through
getApiErrorKeyanduseToast, standardizing failure feedback across the application.
Frequently Asked Questions
How does Open-Notebook handle authentication in API requests?
The frontend/src/lib/api/client.ts file retrieves tokens from the persisted auth-storage store and injects them into Axios request headers. This happens transparently for all domain modules like frontend/src/lib/api/notebooks.ts, ensuring authenticated calls without repeating auth logic in every hook.
What is the difference between TanStack Query and Zustand in this architecture?
TanStack Query manages server-derived state—data that originates from the FastAPI backend and requires network synchronization. It handles caching, refetching, and mutation side effects. Zustand manages client-only state—UI preferences like themes and view modes that never need to sync with the backend but must persist across sessions using localStorage.
How does cache invalidation work when creating or updating notebooks?
After a successful mutation in useCreateNotebook or useUpdateNotebook, the hook calls queryClient.invalidateQueries() with the specific QUERY_KEYS (e.g., QUERY_KEYS.notebooks or QUERY_KEYS.notebook(id)). This marks cached data as stale, triggering automatic refetching in any mounted component that subscribes to those keys.
Why does the API client use a 10-minute timeout?
The timeout configured in frontend/src/lib/api/client.ts accommodates long-running LLM operations common in Open-Notebook's workflow, such as generating summaries or processing sources. Standard HTTP timeouts would interrupt these legitimate backend processes, so the extended duration ensures completion without false error states.
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 →