Web DAL Layer Implementation Plan
Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев
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
cd apps/web && pnpm add axios
- Step 2: Commit
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
// 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
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
// 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
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
// 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
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
// 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
// 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
// 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
// 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
// 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
// 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
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
// 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
// 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
// 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
// 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
// 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
// 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
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
// 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
// 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
// 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
// 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
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:
// 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
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:
serverFetchcallscookies()fromnext/headers, which makes routes dynamic. This is intentional — cookie forwarding is needed for authenticated pages. For these two public pages (catalog, item detail), therevalidateoption 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:
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:
// 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):
// 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):
// old
data = (await api.catalog.getItemBySlug(slug)) as ItemDetailResponse;
// new
data = await serverFetch<ItemDetailResponse>(`/catalog/items/${slug}`, { revalidate: 600 });
- Step 4: Verify build
cd apps/web && pnpm build
Expected: no import errors.
- Step 5: Commit
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"