VayuUI

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-query

Use one-way layers:

types/api-types -> api/services -> api/hooks -> UI

UI 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

ArtifactPathRule
HTTP client/interceptorsapi/api.tsAxios/fetch singleton; auth headers, refresh, global 401/403/500 handling.
API typestypes/api-types/Shared request/response/params contracts; never redefine in UI/hooks.
Service functionsapi/services/<feature>Service.tsEndpoint path, method, params, payload; no React/cache/UI logic.
Query keys/hooksapi/hooks/One hook per file plus key factory per feature.
UI consumptioncontainers/, 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

ArtifactPatternExample
Service file<feature>Service.tsuserService.ts
List serviceget<FeaturePlural>getUsers
Detail serviceget<Feature>DetailgetUserDetail
Create/update/deletecreate<Feature>, update<Feature>, delete<Feature>updateUser
Query hookuseGet<FeaturePlural>.tsuseGetUsers.ts
Detail hookuseGet<Feature>Detail.tsuseGetUserDetail.ts
Mutation hookuseCreate<Feature>.ts, useUpdate<Feature>.tsuseCreateUser.ts
Types<APIname><Request/Response>.ts or feature API fileGetUsersRequest.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 enabled guard.
  • UI that only handles success state.

On this page