Use when creating API requests, data fetching hooks, or mutations. Defines the single API client pattern, react-query query/mutation structures, Zod validation, and data prefetching.
Use this skill when you need to:
Create ONE pre-configured API client instance in src/lib/api-client.ts. All API calls go through this instance.
// src/lib/api-client.ts
import Axios, { InternalAxiosRequestConfig } from 'axios';
import { useNotifications } from '@/components/ui/notifications';
import { env } from '@/config/env';
import { paths } from '@/config/paths';
function authRequestInterceptor(config: InternalAxiosRequestConfig) {
if (config.headers) {
config.headers.Accept = 'application/json';
}
config.withCredentials = true;
return config;
}
export const api = Axios.create({
baseURL: env.API_URL,
});
api.interceptors.request.use(authRequestInterceptor);
api.interceptors.response.use(
(response) => response.data,
(error) => {
const message = error.response?.data?.message || error.message;
useNotifications.getState().addNotification({
type: 'error',
title: 'Error',
message,
});
if (error.response?.status === 401) {
window.location.href = paths.auth.login.getHref(window.location.pathname);
}
return Promise.reject(error);
},
);
true for cookie-based authresponse.data so consumers get data directlyPre-configure react-query in src/lib/react-query.ts:
// src/lib/react-query.ts
import { DefaultOptions, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query';
export const queryConfig: DefaultOptions = {
queries: {
refetchOnWindowFocus: false,
retry: false,
staleTime: 1000 * 60, // 1 minute
},
};
export type QueryConfig<T extends (...args: any[]) => any> = Omit<
ReturnType<T>,
'queryKey' | 'queryFn'
>;
export type MutationConfig<T extends (...args: any[]) => any> = UseMutationOptions<
Awaited<ReturnType<T>>,
Error,
Parameters<T>[0]
>;
Every query follows this exact structure within src/features/<feature>/api/:
// src/features/<feature>/api/get-<resource>.ts
import { queryOptions, useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api-client';
import { QueryConfig } from '@/lib/react-query';
import { Resource, Meta } from '@/types/api';
// 1. Fetcher function
export const getResources = (page = 1): Promise<{ data: Resource[]; meta: Meta }> => {
return api.get('/resources', { params: { page } });
};
// 2. Query options factory
export const getResourcesQueryOptions = ({ page }: { page?: number } = {}) => {
return queryOptions({
queryKey: page ? ['resources', { page }] : ['resources'],
queryFn: () => getResources(page),
});
};
// 3. Custom hook
type UseResourcesOptions = {
page?: number;
queryConfig?: QueryConfig<typeof getResourcesQueryOptions>;
};
export const useResources = ({ queryConfig, page }: UseResourcesOptions) => {
return useQuery({
...getResourcesQueryOptions({ page }),
...queryConfig,
});
};
queryKey and queryFn. Exported so it can be used for prefetching and cache invalidation.useQuery and accepts optional queryConfig for consumer customization.// src/features/<feature>/api/get-<resource>.ts
export const getResource = ({ resourceId }: { resourceId: string }): Promise<Resource> => {
return api.get(`/resources/${resourceId}`);
};
export const getResourceQueryOptions = (resourceId: string) => {
return queryOptions({
queryKey: ['resources', resourceId],
queryFn: () => getResource({ resourceId }),
});
};
export const useResource = ({ resourceId, queryConfig }: UseResourceOptions) => {
return useQuery({
...getResourceQueryOptions(resourceId),
...queryConfig,
});
};
Every mutation follows this exact structure:
// src/features/<feature>/api/create-<resource>.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';
import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';
import { Resource } from '@/types/api';
import { getResourcesQueryOptions } from './get-resources';
// 1. Input validation schema
export const createResourceInputSchema = z.object({
title: z.string().min(1, 'Required'),
body: z.string().min(1, 'Required'),
});
export type CreateResourceInput = z.infer<typeof createResourceInputSchema>;
// 2. Mutation function
export const createResource = ({ data }: { data: CreateResourceInput }): Promise<Resource> => {
return api.post('/resources', data);
};
// 3. Custom hook with cache invalidation
type UseCreateResourceOptions = {
mutationConfig?: MutationConfig<typeof createResource>;
};
export const useCreateResource = ({ mutationConfig }: UseCreateResourceOptions = {}) => {
const queryClient = useQueryClient();
const { onSuccess, ...restConfig } = mutationConfig || {};
return useMutation({
onSuccess: (...args) => {
queryClient.invalidateQueries({
queryKey: getResourcesQueryOptions().queryKey,
});
onSuccess?.(...args);
},
...restConfig,
mutationFn: createResource,
});
};
useMutation, handles cache invalidation, and allows consumer callbacks.// Update
export const useUpdateResource = ({ mutationConfig }) => {
const queryClient = useQueryClient();
return useMutation({
onSuccess: (data, variables) => {
queryClient.refetchQueries({ queryKey: getResourceQueryOptions(variables.resourceId).queryKey });
queryClient.invalidateQueries({ queryKey: getResourcesQueryOptions().queryKey });
},
mutationFn: updateResource,
});
};
// Delete
export const useDeleteResource = ({ mutationConfig }) => {
const queryClient = useQueryClient();
return useMutation({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getResourcesQueryOptions().queryKey });
},
mutationFn: deleteResource,
});
};
Prefetch data before navigation for better UX:
// In a list component — prefetch on hover
import { useQueryClient } from '@tanstack/react-query';
import { getResourceQueryOptions } from '../api/get-resource';
const queryClient = useQueryClient();
<Link
onMouseEnter={() => queryClient.prefetchQuery(getResourceQueryOptions(resource.id))}
to={`/resources/${resource.id}`}
>
{resource.title}
</Link>
src/features/<feature>/api/
├── get-<resources>.ts # List query (plural)
├── get-<resource>.ts # Single item query (singular)
├── create-<resource>.ts # Create mutation
├── update-<resource>.ts # Update mutation
├── delete-<resource>.ts # Delete mutation
src/features/<feature>/api/api clientqueryOptions factory + useQuery hookuseMutation hook with proper cache invalidationz.infer<typeof schema>QueryConfig/MutationConfig types for hook options