Skip to content

API Layer Pattern

Organizing API calls with TanStack Query.

Structure

src/api/{feature}/
├── keys.ts       # Query key factories
├── queries.ts    # useQuery hooks
└── mutations.ts  # useMutation hooks

Query Keys

export const markerKeys = {
  all: ["markers"] as const,
  lists: () => [...markerKeys.all, "list"] as const,
  list: (filters: MarkerFilters) => [...markerKeys.lists(), filters] as const,
  details: () => [...markerKeys.all, "detail"] as const,
  detail: (id: string) => [...markerKeys.details(), id] as const,
};

Query Hook

export const useMarkers = (filters: MarkerFilters) =>
  useQuery({
    queryKey: markerKeys.list(filters),
    queryFn: () => apiCall<MarkerResponse[]>(
      getApiV1Markers({ query: filters })
    ),
  });

export const useMarker = (id: string) =>
  useQuery({
    queryKey: markerKeys.detail(id),
    queryFn: () => apiCall<MarkerResponse>(
      getApiV1MarkersId({ path: { id } })
    ),
    enabled: !!id,
  });

Mutation Hook

export const useCreateMarker = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateMarkerRequest) =>
      apiCall<MarkerResponse>(postApiV1Markers({ body: data })),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: markerKeys.lists() });
    },
    meta: { invalidateOnConflict: [markerKeys.all] },
  });
};

API Call Wrapper

export const apiCall = async <T>(promise: Promise<Response>): Promise<T> => {
  const response = await promise;

  if (!response.ok) {
    throw new HttpError(response.status, await response.json());
  }

  return response.json();
};

Error Handling

const mutation = useCreateMarker();

mutation.mutate(data, {
  onError: (error) => {
    if (error instanceof HttpError) {
      if (error.status === 409) {
        queryClient.invalidateQueries({ queryKey: markerKeys.all });
      }
      showErrorToast(error.message);
    }
  },
});