> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evomarketing.co/llms.txt
> Use this file to discover all available pages before exploring further.

# API client

> Frontend API client layer and data fetching patterns

The frontend uses a typed API client layer with TanStack Query for server state management.

## API client structure

```
lib/api/
  internal.ts       # Admin/internal API (7,800+ lines)
  creator.ts        # Creator-facing API
  community-chat.ts # Chat API
  client-hub.ts     # Client hub API
  ambassador.ts     # Ambassador API
```

## Base utilities

**File:** `lib/utils.ts`

```typescript theme={null}
export const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";

export async function apiFetch(path: string, init: RequestInit = {}) {
  const token = await getCachedToken();
  const headers = new Headers(init.headers);
  if (token) headers.set("Authorization", `Bearer ${token}`);
  return fetch(`${API_URL}${path}`, { ...init, headers });
}

export async function getJson<T>(path: string): Promise<T> {
  const res = await apiFetch(path);
  if (!res.ok) throw new ApiError(res.status, await res.text());
  return res.json();
}

export async function postJson<T>(path: string, body: unknown): Promise<T> {
  const res = await apiFetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new ApiError(res.status, await res.text());
  return res.json();
}
```

## Query pattern

Each API file exports typed query functions:

```typescript theme={null}
// lib/api/internal.ts

export interface Brand {
  id: number;
  name: string;
  pipelineStatus: string;
  // ...
}

export const brandsQuery = () =>
  queryOptions({
    queryKey: ["brands"],
    queryFn: () => getJson<Brand[]>("/api/internal/brands"),
  });

export const brandQuery = (id: number) =>
  queryOptions({
    queryKey: ["brand", id],
    queryFn: () => getJson<Brand>(`/api/internal/brands/${id}`),
  });
```

## Using queries

### In components

```typescript theme={null}
import { useQuery } from "@tanstack/react-query";
import { brandsQuery, brandQuery } from "@/lib/api/internal";

function BrandList() {
  const { data: brands, isLoading } = useQuery(brandsQuery());
  
  if (isLoading) return <Spinner />;
  return brands.map(brand => <BrandCard key={brand.id} brand={brand} />);
}

function BrandDetail({ id }: { id: number }) {
  const { data: brand } = useQuery(brandQuery(id));
  return <div>{brand?.name}</div>;
}
```

### With suspense

```typescript theme={null}
import { useSuspenseQuery } from "@tanstack/react-query";

function BrandList() {
  const { data: brands } = useSuspenseQuery(brandsQuery());
  return brands.map(brand => <BrandCard key={brand.id} brand={brand} />);
}
```

## Mutation pattern

```typescript theme={null}
// lib/api/internal.ts

export async function updateBrand(id: number, data: Partial<Brand>) {
  return patchJson<Brand>(`/api/internal/brands/${id}`, data);
}

export async function createBrand(data: NewBrand) {
  return postJson<Brand>("/api/internal/brands", data);
}
```

### In components

```typescript theme={null}
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateBrand } from "@/lib/api/internal";

function BrandEditor({ brand }: { brand: Brand }) {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (data: Partial<Brand>) => updateBrand(brand.id, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["brand", brand.id] });
      queryClient.invalidateQueries({ queryKey: ["brands"] });
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      mutation.mutate({ name: "New Name" });
    }}>
      {/* form fields */}
    </form>
  );
}
```

## Server prefetching

**File:** `lib/server/query-hydration.tsx`

For SSR, prefetch queries server-side:

```typescript theme={null}
import { HydratedRoute } from "@/lib/server/query-hydration";
import { brandsQuery } from "@/lib/api/internal";

export default async function BrandsPage() {
  return (
    <HydratedRoute queries={[brandsQuery()]}>
      <BrandsClient />
    </HydratedRoute>
  );
}
```

This:

1. Prefetches data on the server
2. Dehydrates query cache
3. Hydrates on client
4. Avoids loading spinners on initial render

## Query configuration

**File:** `components/providers.tsx`

```typescript theme={null}
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,  // 1 minute
      refetchOnWindowFocus: false,
    },
  },
});
```

## Real-time invalidation

ActionCable subscriptions invalidate queries on updates:

```typescript theme={null}
// components/app-shell/realtime-subscriptions.tsx

useEffect(() => {
  const subscription = cable.subscriptions.create(
    { channel: "NotificationsChannel" },
    {
      received(data) {
        queryClient.invalidateQueries({ queryKey: ["notifications"] });
      },
    }
  );
  return () => subscription.unsubscribe();
}, []);
```

## Error handling

```typescript theme={null}
export class ApiError extends Error {
  constructor(public status: number, public body: string) {
    super(`API Error ${status}: ${body}`);
  }
}

// In components
const { error } = useQuery(someQuery());
if (error instanceof ApiError && error.status === 404) {
  return <NotFound />;
}
```

## Type generation

API types are manually maintained in `lib/api/*.ts`. Keep them in sync with backend response shapes.
