Complete reference for backend API integration in the MemexLLM frontend.
The API client layer provides type-safe HTTP communication with the backend. It handles authentication, rate limiting, error handling, and streaming responses.
┌─────────────────────────────────────────────────────────┐
│ API Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ client │ │ auth │ │ rate-limit │ │
│ │ (base) │ │ (token) │ │ (handling) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
└──────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Domain APIs │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ notebooks│ │ chat │ │documents │ │generation│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ notes │ │ tasks │ │ feedback │ │ auth │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
File: lib/api/client.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
const DEFAULT_TIMEOUT_MS = 30000; // 30 seconds
const DEFAULT_MAX_RETRIES = 3;// Generic API error
class ApiError extends Error {
constructor(
public status: number,
public detail: string,
public rateLimitStatus?: RateLimitStatus
) {
super(detail);
this.name = "ApiError";
}
}
// Rate limit specific error
class RateLimitError extends ApiError {
constructor(
public retryAfterSeconds: number | null,
public rateLimitStatus: RateLimitStatus,
detail: string = "Too many requests. Please try again later."
) {
super(429, detail, rateLimitStatus);
this.name = "RateLimitError";
}
}async function apiClient<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T>Features:
- Automatic JWT token injection
- JSON parsing
- Error handling
- Rate limit tracking
- Request timeout
- Retry logic
import { apiClient, ApiError } from "@/lib/api/client";
try {
const data = await apiClient<Notebook[]>('/notebooks');
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.status}: ${error.detail}`);
}
}async function apiUpload<T>(
endpoint: string,
formData: FormData
): Promise<T>Usage:
const formData = new FormData();
formData.append("file", file);
formData.append("notebook_id", notebookId);
const result = await apiUpload<UploadDocumentResponse>(
"/documents/upload",
formData
);async function getStreamingHeaders(): Promise<HeadersInit>
function getApiBaseUrl(): stringUsage:
const headers = await getStreamingHeaders();
const response = await fetch(`${getApiBaseUrl()}/api/v1/chat/${notebookId}/message`, {
method: "POST",
headers,
body: JSON.stringify({ message, stream: true }),
});
const reader = response.body?.getReader();
// Process SSE streamFile: lib/api/notebooks.ts
export const notebooksApi = {
list: () => apiClient<Notebook[]>("/notebooks"),
get: (id: string) => apiClient<Notebook>(`/notebooks/${id}`),
create: (data: CreateNotebookRequest) =>
apiClient<Notebook>("/notebooks", {
method: "POST",
body: JSON.stringify(data),
}),
update: (id: string, data: UpdateNotebookRequest) =>
apiClient<Notebook>(`/notebooks/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
}),
delete: (id: string) =>
apiClient<void>(`/notebooks/${id}`, { method: "DELETE" }),
};interface Notebook {
id: string;
user_id: string;
title: string;
settings: NotebookRAGSettings;
created_at: string;
updated_at: string;
source_count?: number;
}
interface CreateNotebookRequest {
title: string;
settings?: NotebookRAGSettings;
}
interface UpdateNotebookRequest {
title?: string;
settings?: NotebookRAGSettings;
}File: lib/api/chat.ts
export const chatApi = {
getHistory: (notebookId: string, limit = 50) =>
apiClient<ChatMessage[]>(`/chat/${notebookId}/history?limit=${limit}`),
deleteHistory: (notebookId: string) =>
apiClient<void>(`/chat/${notebookId}/history`, { method: "DELETE" }),
getSuggestions: (notebookId: string) =>
apiClient<SuggestionsResponse>(`/chat/${notebookId}/suggestions`),
getConversationSuggestions: async (
notebookId: string,
lastUserMessage: string,
lastAssistantMessage: string
): Promise<string[]>,
sendMessage: (notebookId: string, message: string) =>
apiClient<ChatResponse>(`/chat/${notebookId}/message`, {
method: "POST",
body: JSON.stringify({ message, stream: false }),
}),
sendMessageStream: async (
notebookId: string,
message: string,
onToken: (token: string) => void,
onCitations?: (citations: Citation[]) => void,
onComplete?: () => void,
onError?: (error: Error) => void,
signal?: AbortSignal
): Promise<void>,
};await chatApi.sendMessageStream(
notebookId,
"What is this document about?",
(token) => {
// Append token to message
setMessage((prev) => prev + token);
},
(citations) => {
// Store citations for display
setCitations(citations);
},
() => {
// Stream complete
setIsStreaming(false);
},
(error) => {
// Handle error
console.error(error);
},
abortSignal // Optional abort controller
);File: lib/api/documents.ts
export const documentsApi = {
list: (notebookId: string) =>
apiClient<Document[]>(`/documents/notebook/${notebookId}`),
upload: (notebookId: string, file: File) => {
const formData = new FormData();
formData.append("file", file);
formData.append("notebook_id", notebookId);
return apiUpload<UploadDocumentResponse>("/documents/upload", formData);
},
processUrl: (notebookId: string, url: string) =>
apiClient<ProcessUrlResponse>("/documents/url", {
method: "POST",
body: JSON.stringify({ notebook_id: notebookId, url }),
}),
getUrl: (documentId: string) =>
apiClient<DocumentUrlResponse>(`/documents/${documentId}/url`),
delete: (documentId: string) =>
apiClient<void>(`/documents/${documentId}`, { method: "DELETE" }),
};File: lib/api/generation.ts
export const generationApi = {
list: (notebookId: string, contentType?: ContentType) =>
apiClient<ContentListResponse>(
`/generation/${notebookId}/content${contentType ? `?content_type=${contentType}` : ""}`
),
generatePodcast: (notebookId: string, documentIds?: string[]) =>
apiClient<GenerateContentResponse>(`/generation/${notebookId}/podcast`, {
method: "POST",
body: JSON.stringify({ document_ids: documentIds }),
}),
generateQuiz: (notebookId: string, documentIds?: string[], numQuestions?: number) =>
apiClient<GenerateContentResponse>(`/generation/${notebookId}/quiz`, {
method: "POST",
body: JSON.stringify({ document_ids: documentIds, num_questions: numQuestions }),
}),
generateFlashcards: (notebookId: string, documentIds?: string[], numCards?: number) =>
apiClient<GenerateContentResponse>(`/generation/${notebookId}/flashcards`, {
method: "POST",
body: JSON.stringify({ document_ids: documentIds, num_cards: numCards }),
}),
generateMindmap: (notebookId: string, documentIds?: string[]) =>
apiClient<GenerateContentResponse>(`/generation/${notebookId}/mindmap`, {
method: "POST",
body: JSON.stringify({ document_ids: documentIds }),
}),
getContent: (contentId: string) =>
apiClient<GeneratedContent>(`/generation/content/${contentId}`),
deleteContent: (contentId: string) =>
apiClient<void>(`/generation/content/${contentId}`, { method: "DELETE" }),
};
export const tasksApi = {
get: (taskId: string) =>
apiClient<TaskProgress>(`/tasks/${taskId}`),
};File: lib/api/notes.ts
export const notesApi = {
list: (notebookId: string) =>
apiClient<Note[]>(`/notebooks/${notebookId}/notes`),
create: (data: CreateNoteRequest) =>
apiClient<Note>(`/notebooks/${data.notebookId}/notes`, {
method: "POST",
body: JSON.stringify({ title: data.title, content: data.content }),
}),
get: (noteId: string) =>
apiClient<Note>(`/notes/${noteId}`),
update: (noteId: string, data: UpdateNoteRequest) =>
apiClient<Note>(`/notes/${noteId}`, {
method: "PATCH",
body: JSON.stringify(data),
}),
delete: (noteId: string) =>
apiClient<void>(`/notes/${noteId}`, { method: "DELETE" }),
};File: lib/api/auth.ts
export const authApi = {
me: () => apiClient<User>("/auth/me"),
};
export const healthApi = {
check: () => apiClient<HealthStatus>("/health"),
detailed: () => apiClient<DetailedHealthStatus>("/health/detailed"),
};File: lib/api/feedback.ts
export async function submitFeedback(
contentType: FeedbackContentType,
contentId: string,
rating: FeedbackRating,
comment?: string
): Promise<void>
export async function getFeedback(contentId: string): Promise<FeedbackResponse>File: lib/api/rate-limit.ts
The client automatically handles rate limiting:
interface RateLimitStatus {
limit: number;
remaining: number;
reset: number;
retryAfter?: number;
}
// Automatic retry with exponential backoff
function calculateBackoffDelay(
retryCount: number,
retryAfterSeconds?: number | null
): number
// Check if endpoint is currently rate limited
function isRateLimited(endpoint: string): boolean
// Get time until rate limit resets
function getRateLimitResetTime(endpoint: string): number | nullThe client reads these headers from responses:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200
Retry-After: 60
function handleAuthError(status: number, endpoint: string): void {
if (status === 401 && typeof window !== "undefined") {
// Log and redirect to login
window.location.href = "/auth/login";
}
}try {
const data = await apiClient<Notebook[]>('/notebooks');
} catch (error) {
if (error instanceof RateLimitError) {
// Show retry countdown
const retryAfter = error.retryAfterSeconds;
toast.error(`Rate limited. Retry in ${retryAfter}s`);
} else if (error instanceof ApiError) {
switch (error.status) {
case 401:
router.push('/auth/login');
break;
case 403:
toast.error('Access denied');
break;
case 404:
toast.error('Not found');
break;
case 500:
toast.error('Server error');
break;
default:
toast.error(error.detail);
}
} else {
toast.error('Network error');
}
}- Type Safety: Always provide generic type parameter
- Error Handling: Use
ApiErrorfor typed error handling - Cancellation: Pass
AbortSignalfor cancellable requests - Rate Limits: Respect rate limits with backoff
- Auth: Client automatically adds JWT token
- Uploads: Use
apiUploadfor multipart/form-data - Streaming: Use SSE for real-time updates