Next.js API Layer
Two Layers — Never Mix
| Context | Module | Pattern |
|---|---|---|
| Server Components (RSC/ISR) | lib/api/*.ts |
fetch() + next: { revalidate } |
| Client + hooks | lib/api-client.ts or utils/api.ts |
Cookie auth, throws on error |
Server Fetch
// lib/api/product.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
export async function getProductBySlug(slug: string) {
const res = await fetch(`${API_BASE_URL}/product-variation/get-product-by-slug?slug=${slug}`, {
next: { revalidate: 300 },
});
return res.json();
}
Use in: page.tsx, generateMetadata, generateStaticParams. No cookie auth on server.
Client API
// utils/api.ts or lib/api-client.ts
export const api = {
async get<T>(path: string, options?) {
const token = Cookies.get('token');
const res = await fetch(`${API_BASE_URL}${path}`, {
headers: { Authorization: token ? `Bearer ${token}` : '', ...capiHeaders() },
credentials: 'include',
});
const json = await res.json();
if (json.key || json.message) throw new Error(json.message);
return json as ApiResponse<T>;
},
};
Service Layer
// services/products.service.ts
export const productsService = {
async getAll(params: ProductsParams) {
const res = await api.get<Product[]>(`/product/get-all-products?${qs}`);
return { products: res.data, totalCount: res.count };
},
};
export const productsKeys = {
all: ['products'] as const,
list: (p) => [...productsKeys.all, 'list', p] as const,
};
React Query Hook
export const useProducts = (params) =>
useQuery({
queryKey: productsKeys.list(params),
queryFn: () => productsService.getAll(params),
staleTime: 2 * 60 * 1000,
});
Response Shape (backend)
{ status: 'success', data: T, count?: number }
Route Handlers (BFF)
app/api/ae-catalog/route.ts — server-side CSV feeds. Not for regular CRUD.
Rules
- Server page fetches initial data → pass as props to Client
- Client refetches via React Query for filters/infinite scroll
- Never call
Cookies.getin Server Components