Skip to main content

Overview

Ship uses TanStack Query v5 with an auto-generated typed API client. The typed client is generated from API endpoints via codegen — no manual hook creation needed. Import the client from services/api-client.service:
import { apiClient } from 'services/api-client.service';

Queries (GET)

import { useApiQuery } from 'hooks';
import { apiClient } from 'services/api-client.service';

// Simple query
const { data, isLoading } = useApiQuery(apiClient.projects.list);

// With parameters
const { data } = useApiQuery(apiClient.projects.list, { page: 1, perPage: 10 });

Mutations (POST/PUT/DELETE)

import { useApiMutation } from 'hooks';

const { mutate, isPending } = useApiMutation(apiClient.projects.create);
mutate({ name: 'New Project' }, { onError: (e) => handleApiError(e, setError) });

Dynamic Path Params

useApiMutation binds pathParams at hook level — they’re fixed for all mutate() calls. For dynamic IDs (e.g., deleting different items in a list), use endpoint.call() directly:
import queryClient from 'query-client';

// ✅ Dynamic pathParams — use .call()
const handleDelete = async (id: string) => {
  await apiClient.projects.remove.call({}, { pathParams: { id } });
  queryClient.invalidateQueries({ queryKey: [apiClient.projects.list.path] });
};

// ✅ Fixed pathParams — hook level is fine
const { mutate: update } = useApiMutation(apiClient.projects.update, {
  pathParams: { id: projectId },
});

Query Invalidation

import queryClient from 'query-client';

queryClient.invalidateQueries({ queryKey: [apiClient.projects.list.path] });
queryClient.setQueryData([apiClient.account.get.path], updatedData);
Query keys = [endpoint.path, ...params]. The first element is always the endpoint path string.

Error Handling

handleApiError(e, setError) from utils:
  • Maps server validation errors → react-hook-form field errors
  • Shows global errors via Sonner toast