HyperSaaS
FrontendData Fetching

Overview

Server components, server actions, and the API proxy pattern.

HyperSaaS uses Next.js server components as the primary data fetching layer. All data flows from the Django backend through authenticated fetch calls, validated with Zod schemas.

Data Flow

Django Backend ← JWT Auth → Next.js Server Component → Client Component (props)


                            Zod schema validation


                            TypeScript type inference

Fetching Patterns

1. Server Component (Primary)

Most pages are server components that fetch data at render time:

export default async function WorkspacePage({ params }) {
  const { workspaceId } = await params;
  const user = await getCurrentUserServer();
  const workspace = await fetchWorkspace(workspaceId);
  const sessions = await getWorkspaceChatSessions(workspaceId);

  return <ChatHistory sessions={sessions} workspace={workspace} />;
}

2. API Route Proxy (Client Components)

Client components that need to make requests (forms, real-time updates) call Next.js API routes, which proxy to the Django backend:

// Client component
const res = await fetch(`/api/workspaces/${workspaceId}/chats`, {
  method: "POST",
  body: JSON.stringify({ name: "New Chat", ai_model: "gpt-4o" }),
});

3. Parallel Fetching

Server components use Promise.all for parallel data fetching:

const [user, workspaces, invitations] = await Promise.all([
  getCurrentUserServer(),
  getUserWorkspaces(),
  getWorkspaceInvitations(workspaceId),
]);

Caching Strategy

Most fetches use cache: "no-store" for real-time data:

const res = await fetch(url, {
  headers: { Authorization: `JWT ${token}` },
  cache: "no-store",
});

Chat session pages also set revalidate = 0 to prevent stale data.

Error Handling

Server Components

const { data, error } = await fetchApiData(endpoint, schema);
if (error) {
  return <ErrorMessage message={error} />;
}

Client Components

try {
  const res = await fetch(url, options);
  if (!res.ok) {
    const data = await res.json();
    toast.error(data.detail || "Something went wrong");
    return;
  }
  // success
} catch (err) {
  toast.error("Network error");
}

Generic Fetch Utilities

fetchApiData

Type-safe fetch with automatic Zod validation:

async function fetchApiData<T>(
  endpoint: string,
  schema: z.ZodSchema<T>
): Promise<{ data: T | null; error?: string }> {
  const token = await getAccessToken();
  const res = await fetch(`${BACKEND_URL}${endpoint}`, {
    headers: { Authorization: `JWT ${token}` },
    cache: "no-store",
  });
  if (!res.ok) return { data: null, error: "Failed to fetch" };

  const json = await res.json();
  const parsed = schema.safeParse(json);
  if (!parsed.success) return { data: null, error: "Invalid response" };

  return { data: parsed.data };
}

authenticatedFetch

Lower-level fetch with JWT injection:

async function authenticatedFetch<T>(
  url: string,
  method: string,
  token: string,
  body?: any
): Promise<T> {
  const res = await fetch(url, {
    method,
    headers: {
      "Content-Type": "application/json",
      Authorization: `JWT ${token}`,
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  return res.json();
}

On this page