# Web DAL Layer Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the monolithic `api.ts` with a structured DAL layer: axios client (with refresh interceptor), server fetch, query key factories, React Query options, and mutation hooks.

**Architecture:** Two transports (axios for client, Next.js fetch for server) feeding shared React Query cache via `queryOptions()` and `HydrationBoundary`. Query keys in separate factories. See spec: `docs/superpowers/specs/2026-03-17-web-dal-layer-design.md`

**Tech Stack:** axios, @tanstack/react-query v5, Next.js 16, TypeScript

---

## Chunk 1: Foundation (transports + error handling)

### Task 1: Install axios

**Files:**
- Modify: `apps/web/package.json`

- [ ] **Step 1: Install axios**

```bash
cd apps/web && pnpm add axios
```

- [ ] **Step 2: Commit**

```bash
git add apps/web/package.json apps/web/pnpm-lock.yaml ../../pnpm-lock.yaml
git commit -m "chore(web): add axios dependency"
```

### Task 2: Create ApiError class

**Files:**
- Create: `apps/web/src/shared/api/errors.ts`

- [ ] **Step 1: Create errors.ts**

```ts
// apps/web/src/shared/api/errors.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly errorCode?: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}
```

- [ ] **Step 2: Commit**

```bash
git add apps/web/src/shared/api/errors.ts
git commit -m "feat(web): add ApiError class for structured error handling"
```

### Task 3: Create axios client with refresh interceptor

**Files:**
- Create: `apps/web/src/shared/api/client.ts`

- [ ] **Step 1: Create client.ts**

```ts
// apps/web/src/shared/api/client.ts
import axios, {
  type AxiosError,
  type InternalAxiosRequestConfig,
} from 'axios';
import { ApiError } from './errors';

export const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5001/api/v1',
  withCredentials: true,
  headers: { 'Content-Type': 'application/json' },
});

let isRefreshing = false;
let failedQueue: Array<{
  resolve: (value?: unknown) => void;
  reject: (reason?: unknown) => void;
}> = [];

function processQueue(error: unknown | null) {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) reject(error);
    else resolve();
  });
  failedQueue = [];
}

function toApiError(error: AxiosError): ApiError {
  const data = error.response?.data as Record<string, unknown> | undefined;
  return new ApiError(
    (data?.message as string) || 'Request failed',
    error.response?.status || 500,
    data?.errorCode as string | undefined,
  );
}

apiClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & {
      _retry?: boolean;
    };

    if (error.response?.status !== 401 || originalRequest._retry) {
      return Promise.reject(toApiError(error));
    }

    if (isRefreshing) {
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then(() => apiClient(originalRequest));
    }

    originalRequest._retry = true;
    isRefreshing = true;

    try {
      await axios.post(
        `${apiClient.defaults.baseURL}/auth/refresh`,
        {},
        { withCredentials: true },
      );
      processQueue(null);
      return apiClient(originalRequest);
    } catch (refreshError) {
      processQueue(refreshError);
      if (typeof window !== 'undefined') {
        window.location.href = '/login';
      }
      return Promise.reject(toApiError(error));
    } finally {
      isRefreshing = false;
    }
  },
);
```

- [ ] **Step 2: Commit**

```bash
git add apps/web/src/shared/api/client.ts
git commit -m "feat(web): add axios client with 401 refresh interceptor"
```

### Task 4: Create server fetch transport

**Files:**
- Create: `apps/web/src/shared/api/server.ts`

- [ ] **Step 1: Create server.ts**

```ts
// apps/web/src/shared/api/server.ts
import { cookies } from 'next/headers';
import { ApiError } from './errors';

const SERVER_API_URL =
  process.env.API_URL ||
  process.env.NEXT_PUBLIC_API_URL ||
  'http://localhost:5001/api/v1';

export async function serverFetch<T>(
  path: string,
  options?: { revalidate?: number | false },
): Promise<T> {
  const cookieStore = await cookies();

  const res = await fetch(`${SERVER_API_URL}${path}`, {
    headers: {
      'Content-Type': 'application/json',
      Cookie: cookieStore.toString(),
    },
    ...(options?.revalidate !== undefined
      ? { next: { revalidate: options.revalidate } }
      : {}),
  });

  if (!res.ok) {
    const data = await res.json().catch(() => ({ message: 'Request failed' }));
    throw new ApiError(
      data.message || `API error: ${res.status}`,
      res.status,
      data.errorCode,
    );
  }

  return res.json() as Promise<T>;
}
```

- [ ] **Step 2: Commit**

```bash
git add apps/web/src/shared/api/server.ts
git commit -m "feat(web): add serverFetch transport with cookie forwarding"
```

## Chunk 2: Query key factories

### Task 5: Create all key factories

**Files:**
- Create: `apps/web/src/shared/api/keys/catalog.keys.ts`
- Create: `apps/web/src/shared/api/keys/seller.keys.ts`
- Create: `apps/web/src/shared/api/keys/leads.keys.ts`
- Create: `apps/web/src/shared/api/keys/reviews.keys.ts`
- Create: `apps/web/src/shared/api/keys/admin.keys.ts`
- Create: `apps/web/src/shared/api/keys/auth.keys.ts`

- [ ] **Step 1: Create catalog.keys.ts**

```ts
// apps/web/src/shared/api/keys/catalog.keys.ts
export const catalogKeys = {
  all: () => ['catalog'] as const,
  items: (params?: Record<string, string>) =>
    [...catalogKeys.all(), 'items', params] as const,
  item: (slug: string) => [...catalogKeys.all(), 'item', slug] as const,
  subjects: () => [...catalogKeys.all(), 'subjects'] as const,
  locations: () => [...catalogKeys.all(), 'locations'] as const,
};
```

- [ ] **Step 2: Create seller.keys.ts**

```ts
// apps/web/src/shared/api/keys/seller.keys.ts
export const sellerKeys = {
  all: () => ['seller'] as const,
  items: () => [...sellerKeys.all(), 'items'] as const,
  item: (id: string) => [...sellerKeys.all(), 'item', id] as const,
};
```

- [ ] **Step 3: Create leads.keys.ts**

```ts
// apps/web/src/shared/api/keys/leads.keys.ts
export const leadsKeys = {
  all: () => ['leads'] as const,
  my: () => [...leadsKeys.all(), 'my'] as const,
  seller: () => [...leadsKeys.all(), 'seller'] as const,
};
```

- [ ] **Step 4: Create reviews.keys.ts**

```ts
// apps/web/src/shared/api/keys/reviews.keys.ts
export const reviewsKeys = {
  all: () => ['reviews'] as const,
  byItem: (slug: string) => [...reviewsKeys.all(), 'item', slug] as const,
};
```

- [ ] **Step 5: Create admin.keys.ts**

```ts
// apps/web/src/shared/api/keys/admin.keys.ts
export const adminKeys = {
  all: () => ['admin'] as const,
  stats: () => [...adminKeys.all(), 'stats'] as const,
  leads: () => [...adminKeys.all(), 'leads'] as const,
  pendingItems: () => [...adminKeys.all(), 'pendingItems'] as const,
  moderationItem: (id: string) =>
    [...adminKeys.all(), 'moderationItem', id] as const,
};
```

- [ ] **Step 6: Create auth.keys.ts**

```ts
// apps/web/src/shared/api/keys/auth.keys.ts
export const authKeys = {
  all: () => ['auth'] as const,
  me: () => [...authKeys.all(), 'me'] as const,
};
```

- [ ] **Step 7: Commit**

```bash
git add apps/web/src/shared/api/keys/
git commit -m "feat(web): add query key factories for all domains"
```

## Chunk 3: Query options

### Task 6: Create all query options

**Files:**
- Create: `apps/web/src/shared/api/queries/catalog.queries.ts`
- Create: `apps/web/src/shared/api/queries/seller.queries.ts`
- Create: `apps/web/src/shared/api/queries/leads.queries.ts`
- Create: `apps/web/src/shared/api/queries/reviews.queries.ts`
- Create: `apps/web/src/shared/api/queries/admin.queries.ts`
- Create: `apps/web/src/shared/api/queries/auth.queries.ts`

- [ ] **Step 1: Create catalog.queries.ts**

```ts
// apps/web/src/shared/api/queries/catalog.queries.ts
import { queryOptions } from '@tanstack/react-query';
import { apiClient } from '../client';
import { catalogKeys } from '../keys/catalog.keys';
import type {
  CatalogResponse,
  ItemDetailResponse,
  Subject,
  Location,
} from '../api-types';

export const catalogQueries = {
  items: (params?: Record<string, string>) =>
    queryOptions({
      queryKey: catalogKeys.items(params),
      queryFn: () =>
        apiClient
          .get<CatalogResponse>('/catalog/items', { params })
          .then((r) => r.data),
      staleTime: 5 * 60 * 1000,
    }),

  itemBySlug: (slug: string) =>
    queryOptions({
      queryKey: catalogKeys.item(slug),
      queryFn: () =>
        apiClient
          .get<ItemDetailResponse>(`/catalog/items/${slug}`)
          .then((r) => r.data),
      staleTime: 10 * 60 * 1000,
    }),

  subjects: () =>
    queryOptions({
      queryKey: catalogKeys.subjects(),
      queryFn: () =>
        apiClient.get<Subject[]>('/catalog/subjects').then((r) => r.data),
      staleTime: 30 * 60 * 1000,
    }),

  locations: () =>
    queryOptions({
      queryKey: catalogKeys.locations(),
      queryFn: () =>
        apiClient.get<Location[]>('/catalog/locations').then((r) => r.data),
      staleTime: 30 * 60 * 1000,
    }),
};
```

- [ ] **Step 2: Create seller.queries.ts**

```ts
// apps/web/src/shared/api/queries/seller.queries.ts
import { queryOptions } from '@tanstack/react-query';
import { apiClient } from '../client';
import { sellerKeys } from '../keys/seller.keys';

export const sellerQueries = {
  items: () =>
    queryOptions({
      queryKey: sellerKeys.items(),
      queryFn: () =>
        apiClient.get('/seller/items').then((r) => r.data),
    }),

  item: (id: string) =>
    queryOptions({
      queryKey: sellerKeys.item(id),
      queryFn: () =>
        apiClient.get(`/seller/items/${id}`).then((r) => r.data),
    }),
};
```

- [ ] **Step 3: Create leads.queries.ts**

```ts
// apps/web/src/shared/api/queries/leads.queries.ts
import { queryOptions } from '@tanstack/react-query';
import { apiClient } from '../client';
import { leadsKeys } from '../keys/leads.keys';
import type { LeadResponse } from '../api-types';

export const leadsQueries = {
  my: () =>
    queryOptions({
      queryKey: leadsKeys.my(),
      queryFn: () =>
        apiClient.get<LeadResponse[]>('/me/leads').then((r) => r.data),
    }),

  seller: () =>
    queryOptions({
      queryKey: leadsKeys.seller(),
      queryFn: () =>
        apiClient.get<LeadResponse[]>('/seller/leads').then((r) => r.data),
    }),
};
```

- [ ] **Step 4: Create reviews.queries.ts**

```ts
// apps/web/src/shared/api/queries/reviews.queries.ts
import { queryOptions } from '@tanstack/react-query';
import { apiClient } from '../client';
import { reviewsKeys } from '../keys/reviews.keys';
import type { ReviewsResponse } from '../api-types';

export const reviewsQueries = {
  byItem: (slug: string) =>
    queryOptions({
      queryKey: reviewsKeys.byItem(slug),
      queryFn: () =>
        apiClient
          .get<ReviewsResponse>(`/catalog/items/${slug}/reviews`)
          .then((r) => r.data),
      staleTime: 5 * 60 * 1000,
    }),
};
```

- [ ] **Step 5: Create admin.queries.ts**

```ts
// apps/web/src/shared/api/queries/admin.queries.ts
import { queryOptions } from '@tanstack/react-query';
import { apiClient } from '../client';
import { adminKeys } from '../keys/admin.keys';
import type {
  AdminStats,
  AdminLead,
  ModerationItem,
  ModerationItemDetail,
} from '../api-types';

export const adminQueries = {
  stats: () =>
    queryOptions({
      queryKey: adminKeys.stats(),
      queryFn: () =>
        apiClient.get<AdminStats>('/admin/stats').then((r) => r.data),
    }),

  leads: () =>
    queryOptions({
      queryKey: adminKeys.leads(),
      queryFn: () =>
        apiClient.get<AdminLead[]>('/admin/leads').then((r) => r.data),
    }),

  pendingItems: () =>
    queryOptions({
      queryKey: adminKeys.pendingItems(),
      queryFn: () =>
        apiClient
          .get<ModerationItem[]>('/admin/moderation/items')
          .then((r) => r.data),
    }),

  moderationItem: (id: string) =>
    queryOptions({
      queryKey: adminKeys.moderationItem(id),
      queryFn: () =>
        apiClient
          .get<ModerationItemDetail>(`/admin/moderation/items/${id}`)
          .then((r) => r.data),
    }),
};
```

- [ ] **Step 6: Create auth.queries.ts**

```ts
// apps/web/src/shared/api/queries/auth.queries.ts
import { queryOptions } from '@tanstack/react-query';
import { apiClient } from '../client';
import { authKeys } from '../keys/auth.keys';

export const authQueries = {
  me: () =>
    queryOptions({
      queryKey: authKeys.me(),
      queryFn: () => apiClient.get('/auth/me').then((r) => r.data),
    }),
};
```

- [ ] **Step 7: Commit**

```bash
git add apps/web/src/shared/api/queries/
git commit -m "feat(web): add query options for all domains"
```

## Chunk 4: Mutation hooks

### Task 7: Create all mutation hooks

**Files:**
- Create: `apps/web/src/shared/api/mutations/seller.mutations.ts`
- Create: `apps/web/src/shared/api/mutations/leads.mutations.ts`
- Create: `apps/web/src/shared/api/mutations/reviews.mutations.ts`
- Create: `apps/web/src/shared/api/mutations/admin.mutations.ts`

- [ ] **Step 1: Create seller.mutations.ts**

```ts
// apps/web/src/shared/api/mutations/seller.mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../client';
import { sellerKeys } from '../keys/seller.keys';
import type { CreateItemDTO, UpdateItemDTO } from '@repo/shared';

export function useCreateItem() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateItemDTO) =>
      apiClient.post('/seller/items', data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: sellerKeys.items() });
    },
  });
}

export function useUpdateItem() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateItemDTO }) =>
      apiClient.put(`/seller/items/${id}`, data).then((r) => r.data),
    onSuccess: (_, { id }) => {
      queryClient.invalidateQueries({ queryKey: sellerKeys.items() });
      queryClient.invalidateQueries({ queryKey: sellerKeys.item(id) });
    },
  });
}

export function useDeleteItem() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiClient.delete(`/seller/items/${id}`).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: sellerKeys.items() });
    },
  });
}

export function useSubmitItem() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiClient.post(`/seller/items/${id}/submit`).then((r) => r.data),
    onSuccess: (_, id) => {
      queryClient.invalidateQueries({ queryKey: sellerKeys.items() });
      queryClient.invalidateQueries({ queryKey: sellerKeys.item(id) });
    },
  });
}

export function useArchiveItem() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiClient.post(`/seller/items/${id}/archive`).then((r) => r.data),
    onSuccess: (_, id) => {
      queryClient.invalidateQueries({ queryKey: sellerKeys.items() });
      queryClient.invalidateQueries({ queryKey: sellerKeys.item(id) });
    },
  });
}
```

- [ ] **Step 2: Create leads.mutations.ts**

```ts
// apps/web/src/shared/api/mutations/leads.mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../client';
import { leadsKeys } from '../keys/leads.keys';

interface CreateLeadData {
  itemId: string;
  type: string;
  name: string;
  phone: string;
  email?: string;
  comment?: string;
  source?: string;
}

export function useCreateLead() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateLeadData) =>
      apiClient.post('/leads', data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: leadsKeys.all() });
    },
  });
}

export function useUpdateLeadStatus() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ id, status }: { id: string; status: string }) =>
      apiClient
        .put(`/seller/leads/${id}/status`, { status })
        .then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: leadsKeys.all() });
    },
  });
}
```

- [ ] **Step 3: Create reviews.mutations.ts**

```ts
// apps/web/src/shared/api/mutations/reviews.mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../client';
import { reviewsKeys } from '../keys/reviews.keys';

interface CreateReviewData {
  itemId: string;
  rating: number;
  text?: string;
}

export function useCreateReview() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateReviewData) =>
      apiClient.post('/me/reviews', data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: reviewsKeys.all() });
    },
  });
}
```

- [ ] **Step 4: Create admin.mutations.ts**

```ts
// apps/web/src/shared/api/mutations/admin.mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../client';
import { adminKeys } from '../keys/admin.keys';
import { catalogKeys } from '../keys/catalog.keys';

export function useApproveItem() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ id, comment }: { id: string; comment?: string }) =>
      apiClient
        .post(`/admin/moderation/items/${id}/approve`, { comment })
        .then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: adminKeys.pendingItems() });
      queryClient.invalidateQueries({ queryKey: adminKeys.stats() });
    },
  });
}

export function useRejectItem() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({
      id,
      reason,
      comment,
    }: {
      id: string;
      reason: string;
      comment?: string;
    }) =>
      apiClient
        .post(`/admin/moderation/items/${id}/reject`, { reason, comment })
        .then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: adminKeys.pendingItems() });
      queryClient.invalidateQueries({ queryKey: adminKeys.stats() });
    },
  });
}

export function useCreateSubject() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: { name: string; description?: string; groupName?: string }) =>
      apiClient.post('/catalog/subjects', data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: catalogKeys.subjects() });
    },
  });
}

export function useUpdateSubject() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({
      id,
      data,
    }: {
      id: string;
      data: { name?: string; description?: string; groupName?: string };
    }) =>
      apiClient.put(`/catalog/subjects/${id}`, data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: catalogKeys.subjects() });
    },
  });
}

export function useDeleteSubject() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiClient.delete(`/catalog/subjects/${id}`).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: catalogKeys.subjects() });
    },
  });
}

export function useCreateLocation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: { name: string; type?: string }) =>
      apiClient.post('/catalog/locations', data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: catalogKeys.locations() });
    },
  });
}

export function useUpdateLocation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({
      id,
      data,
    }: {
      id: string;
      data: { name?: string; type?: string };
    }) =>
      apiClient.put(`/catalog/locations/${id}`, data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: catalogKeys.locations() });
    },
  });
}

export function useDeleteLocation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiClient.delete(`/catalog/locations/${id}`).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: catalogKeys.locations() });
    },
  });
}
```

- [ ] **Step 5: Commit**

```bash
git add apps/web/src/shared/api/mutations/
git commit -m "feat(web): add mutation hooks for all domains"
```

## Chunk 5: Barrel export + cleanup

### Task 8: Update barrel export and remove old api.ts

**Files:**
- Modify: `apps/web/src/shared/api/index.ts`
- Delete: `apps/web/src/shared/api/api.ts`

- [ ] **Step 1: Update index.ts**

Replace the contents of `apps/web/src/shared/api/index.ts` with:

```ts
// apps/web/src/shared/api/index.ts

// Transports
export { apiClient } from './client';
export { serverFetch } from './server';
export { ApiError } from './errors';

// Keys
export { catalogKeys } from './keys/catalog.keys';
export { sellerKeys } from './keys/seller.keys';
export { leadsKeys } from './keys/leads.keys';
export { reviewsKeys } from './keys/reviews.keys';
export { adminKeys } from './keys/admin.keys';
export { authKeys } from './keys/auth.keys';

// Queries
export { catalogQueries } from './queries/catalog.queries';
export { sellerQueries } from './queries/seller.queries';
export { leadsQueries } from './queries/leads.queries';
export { reviewsQueries } from './queries/reviews.queries';
export { adminQueries } from './queries/admin.queries';
export { authQueries } from './queries/auth.queries';

// Mutations
export {
  useCreateItem,
  useUpdateItem,
  useDeleteItem,
  useSubmitItem,
  useArchiveItem,
} from './mutations/seller.mutations';
export { useCreateLead, useUpdateLeadStatus } from './mutations/leads.mutations';
export { useCreateReview } from './mutations/reviews.mutations';
export {
  useApproveItem,
  useRejectItem,
  useCreateSubject,
  useUpdateSubject,
  useDeleteSubject,
  useCreateLocation,
  useUpdateLocation,
  useDeleteLocation,
} from './mutations/admin.mutations';

// Types
export type {
  CatalogItem,
  CatalogResponse,
  SellerProfile,
  ItemDetail,
  ItemDetailResponse,
  Subject,
  Location,
  ReviewResponse,
  ReviewsResponse,
  ModerationItem,
  ModerationItemDetail,
  ModerationResult,
  AdminStats,
  AdminLead,
  LeadResponse,
} from './api-types';
```

- [ ] **Step 2: Delete old api.ts**

```bash
rm apps/web/src/shared/api/api.ts
```

- [ ] **Step 3: Fix imports in consumers**

Two files import the `api` object directly. Replace with `serverFetch`.

> **Note:** `serverFetch` calls `cookies()` from `next/headers`, which makes routes dynamic.
> This is intentional — cookie forwarding is needed for authenticated pages.
> For these two public pages (catalog, item detail), the `revalidate` option still controls
> how often the fetch result is cached on the server, so performance impact is minimal.

**`apps/web/src/app/page.tsx`** — full updated file:

```tsx
import { serverFetch, type CatalogResponse } from '@/shared/api';
import { Header } from '@/widgets/header';
import { HomeView } from '@/views/home';

export default async function HomePage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const params = await searchParams;

  const cleanParams: Record<string, string> = {};
  for (const [key, value] of Object.entries(params)) {
    if (typeof value === 'string' && value) {
      cleanParams[key] = value;
    }
  }

  let catalogData: CatalogResponse | null = null;
  try {
    const query = Object.keys(cleanParams).length
      ? `?${new URLSearchParams(cleanParams)}`
      : '';
    catalogData = await serverFetch<CatalogResponse>(`/catalog/items${query}`, {
      revalidate: 300,
    });
  } catch {
    // API may not be running yet — show empty state
  }

  return (
    <>
      <Header />
      <HomeView items={catalogData?.items ?? []} total={catalogData?.total ?? 0} />
    </>
  );
}
```

**`apps/web/src/views/item-detail/ui/ItemDetailPage.tsx`** — change only the import and the two fetch calls:

Replace import:
```ts
// old
import { api, type ItemDetailResponse, type ItemDetail } from '@/shared/api';
// new
import { serverFetch, type ItemDetailResponse, type ItemDetail } from '@/shared/api';
```

Replace in `generateMetadata` (line 53):
```ts
// old
const data = (await api.catalog.getItemBySlug(slug)) as ItemDetailResponse;
// new
const data = await serverFetch<ItemDetailResponse>(`/catalog/items/${slug}`, { revalidate: 600 });
```

Replace in `ItemDetailPage` component (line 81):
```ts
// old
data = (await api.catalog.getItemBySlug(slug)) as ItemDetailResponse;
// new
data = await serverFetch<ItemDetailResponse>(`/catalog/items/${slug}`, { revalidate: 600 });
```

- [ ] **Step 4: Verify build**

```bash
cd apps/web && pnpm build
```

Expected: no import errors.

- [ ] **Step 5: Commit**

```bash
git add -A apps/web/src/shared/api/ apps/web/src/app/page.tsx apps/web/src/views/item-detail/ui/ItemDetailPage.tsx
git commit -m "feat(web): update barrel export, remove old api.ts, fix consumers"
```
