TanStack Query API Integration
Enforce strict layered HTTP API integration with service functions and TanStack Query hooks.
TanStack Query API Integration
This skill enforces a strict layered architecture for HTTP API integration. It ensures every API call flows through typed service functions and TanStack Query hooks, making data-fetching predictable, testable, and cache-safe.
npx skills add Rugved1652/vayu-ui/api-call-tanstack-queryUse one-way layers:
types/api-types -> api/services -> api/hooks -> UIUI imports hooks only. Hooks import services and query keys only. Services import api/api.ts (the HTTP client) and API types only. Services never use React/TanStack Query; UI never imports axios/fetch clients or service functions.
File Placement
| Artifact | Path | Rule |
|---|---|---|
| HTTP client/interceptors | api/api.ts | Axios/fetch singleton; auth headers, refresh, global 401/403/500 handling. |
| API types | types/api-types/ | Shared request/response/params contracts; never redefine in UI/hooks. |
| Service functions | api/services/<feature>Service.ts | Endpoint path, method, params, payload; no React/cache/UI logic. |
| Query keys/hooks | api/hooks/ | One hook per file plus key factory per feature. |
| UI consumption | containers/, components/, app/ | Consume hooks and render loading/error/empty/success states. |
Follow folder-structure, code-quality, and react-hook-form-zod-validation for forms/mutations.
Query Keys
- Use a feature key factory; never hand-build keys in components.
- Include every server-impacting param in the key.
- Keep list/detail keys distinct.
export const featureKeys = {
all: ['features'] as const,
lists: () => [...featureKeys.all, 'list'] as const,
list: (params: GetFeatureListParams) => [...featureKeys.lists(), params] as const,
details: () => [...featureKeys.all, 'detail'] as const,
detail: (id: string) => [...featureKeys.details(), id] as const,
};Services
export const getFeatures = (params: GetFeatureListParams) =>
api.get<FeatureListResponse>('/features', { params });
export const getFeatureDetail = (id: string) =>
api.get<FeatureDetailResponse>(`/features/${id}`);
export const createFeature = (payload: CreateFeatureRequest) =>
api.post<FeatureResponse>('/features', payload);
export const updateFeature = ({ id, payload }: UpdateFeatureVariables) =>
api.patch<FeatureResponse>(`/features/${id}`, payload);
export const deleteFeature = (id: string) => api.delete<void>(`/features/${id}`);Service functions are thin, typed HTTP calls. Put param serialization, URL construction, and payload shape here.
Query Hooks
export const useGetFeatures = (params: GetFeatureListParams) =>
useQuery({
queryKey: featureKeys.list(params),
queryFn: () => getFeatures(params),
refetchOnWindowFocus: false,
});
export const useGetFeatureDetail = (id?: string) =>
useQuery({
queryKey: featureKeys.detail(id ?? ''),
queryFn: () => getFeatureDetail(id as string),
enabled: !!id,
refetchOnWindowFocus: false,
});Use enabled for detail/dependent queries that need an id, open state, auth state, or selected row. Default refetchOnWindowFocus: false; opt in only when the feature needs tab-focus freshness.
Mutation Hooks
export const useCreateFeature = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createFeature,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: featureKeys.lists() });
},
});
};
export const useUpdateFeature = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateFeature,
onSuccess: async (_data, variables) => {
await queryClient.invalidateQueries({ queryKey: featureKeys.lists() });
await queryClient.invalidateQueries({ queryKey: featureKeys.detail(variables.id) });
},
});
};Invalidation rules:
- Create: invalidate lists.
- Update: invalidate lists and changed detail.
- Delete: invalidate lists and remove/invalidate deleted detail when cached.
- Never call unfiltered
queryClient.invalidateQueries()unless the whole app cache is intentionally stale.
UI Consumption
const { data, isLoading, error } = useGetFeatures(params);
if (isLoading) return <Skeleton />;
if (error) return <ErrorState error={error} />;
if (!data?.data.length) return <EmptyState />;
return <FeatureList items={data.data} />;Every consuming UI handles loading, error, empty, and success. UI may format/display data and call hook mutation functions; it must not know transport details or construct query keys.
Naming
| Artifact | Pattern | Example |
|---|---|---|
| Service file | <feature>Service.ts | userService.ts |
| List service | get<FeaturePlural> | getUsers |
| Detail service | get<Feature>Detail | getUserDetail |
| Create/update/delete | create<Feature>, update<Feature>, delete<Feature> | updateUser |
| Query hook | useGet<FeaturePlural>.ts | useGetUsers.ts |
| Detail hook | useGet<Feature>Detail.ts | useGetUserDetail.ts |
| Mutation hook | useCreate<Feature>.ts, useUpdate<Feature>.ts | useCreateUser.ts |
| Types | <APIname><Request/Response>.ts or feature API file | GetUsersRequest.ts |
Error Boundaries
- Global interceptor handles cross-cutting auth/server behavior: 401, 403, refresh, generic 500.
- Hooks expose
error; UI decides inline state, toast, or boundary. - Avoid duplicate toasts when interceptor already handles a status code.
Review Checklist
- API types live in shared
types/api-types/. - Services are typed, thin, and free of React/cache/UI logic.
- One hook per file; hooks import services, not HTTP clients.
- Query keys include all server-impacting params.
- Detail/dependent queries use
enabled. - Mutations invalidate only relevant keys.
- UI imports hooks only and handles loading/error/empty/success.
- No direct axios/fetch/service calls in UI.
Anti-Patterns
- API types inside components or hooks.
- Multiple unrelated hooks in one file.
- HTTP client usage from UI or hooks.
- Missing params from query keys.
- Over-invalidating with unfiltered
invalidateQueries(). - Fetching detail data without an
enabledguard. - UI that only handles success state.