Admin API Layer
Axios Client
// lib/api.ts
export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4000';
const api = axios.create({ baseURL: API_URL });
// No default Content-Type (FormData needs auto boundary)
// Bearer from cookie; 401 → clear auth → /login?redirect=
Typed env: config/env.ts
Feature API Structure
features/category/api/
├── keys.ts # query key factory
├── api.ts # raw axios calls
├── queries.ts # useQuery hooks
└── mutations.ts # useMutation hooks
Query Keys
export const categoryKeys = {
all: ['categories'] as const,
lists: () => [...categoryKeys.all, 'list'] as const,
list: (f: CategoryFilters) => [...categoryKeys.lists(), f] as const,
detail: (id: string) => [...categoryKeys.all, 'detail', id] as const,
};
Query Hook
export const useGetCategories = (filters: CategoryFilters) =>
useQuery({
queryKey: categoryKeys.list(filters),
queryFn: () => getCategories(filters),
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
placeholderData: (prev) => prev,
});
Mutation Hook
export const useDeleteCategory = () => {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteCategory(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: categoryKeys.all });
toast.success('Deleted');
},
onError: (e: any) => toast.error(e?.response?.data?.message),
});
};
Backend Response
{ status: 'success', data: T[], count: number } // list
{ status: 'success', data: T } // single
error.response?.data?.message | .key
Backend URL Pattern
| Action | URL |
|---|---|
| List | GET /{module}/get-all-{resources}?page=&pageItemsCount=&search= |
| Get one | GET /{module}/get-{resource}?id= or ?slug= |
| Create | POST /{module}/create-{resource} |
| Update | PATCH /{module}/update-{resource}?id= |
| Delete | DELETE /{module}/delete-{resource}?id= |
Reference Data
staleTime: Infinity for countries, enums — useCountryStore pattern (React Query, not Zustand).
Images
Display: `${API_URL}${entity.imageUrl}`