import { PatternSections, resolvePattern } from './pattern-helper';
// ─── Full sections (with code examples) ─────────────────────────────
const sections: Record<string, string> = {
'axios-client': `## Axios Client with Interceptors
\`\`\`tsx
// src/services/api/client.ts
import axios from 'axios';
import { useAuthStore } from '@/store/auth';
const apiClient = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
// Request interceptor — automatically adds auth token
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = \`Bearer \${token}\`;
}
return config;
});
// Response interceptor — auto-logout on 401
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
export default apiClient;
\`\`\``,
services: `## API Service Pattern
Group API calls by domain in separate files:
\`\`\`tsx
// src/services/api/auth.ts
import apiClient from './client';
type LoginResponse = {
token: string;
user: { id: string; name: string; email: string };
};
export const authService = {
login: async (email: string, password: string): Promise<LoginResponse> => {
const { data } = await apiClient.post('/auth/login', { email, password });
return data;
},
register: async (payload: {
name: string;
email: string;
password: string;
}): Promise<LoginResponse> => {
const { data } = await apiClient.post('/auth/register', payload);
return data;
},
getProfile: async (): Promise<User> => {
const { data } = await apiClient.get('/auth/profile');
return data;
},
};
\`\`\`
\`\`\`tsx
// src/services/api/products.ts
import apiClient from './client';
export const productService = {
getAll: async (params?: { page?: number; limit?: number }) => {
const { data } = await apiClient.get('/products', { params });
return data;
},
getById: async (id: string) => {
const { data } = await apiClient.get(\`/products/\${id}\`);
return data;
},
create: async (product: CreateProductPayload) => {
const { data } = await apiClient.post('/products', product);
return data;
},
};
\`\`\``,
'query-hooks': `## TanStack Query — Custom Query Hooks
> **Version note**: Examples below use **TanStack Query v5** (current). See diff at the bottom if your project uses v4.
Always create custom hooks for TanStack Query. Do NOT use useQuery/useMutation directly in components.
\`\`\`tsx
// src/hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';
import { productService } from '@/services/api/products';
export const useProducts = (page = 1) => {
return useQuery({
queryKey: ['products', page],
queryFn: () => productService.getAll({ page, limit: 20 }),
});
};
export const useProduct = (id: string) => {
return useQuery({
queryKey: ['product', id],
queryFn: () => productService.getById(id),
enabled: !!id, // Don't fetch if id is missing
});
};
\`\`\``,
'mutation-hooks': `## TanStack Query — Custom Mutation Hooks
\`\`\`tsx
// src/hooks/useAuth.ts
import { useMutation } from '@tanstack/react-query';
import { authService } from '@/services/api/auth';
import { useAuthStore } from '@/store/auth';
export const useLogin = () => {
const login = useAuthStore((s) => s.login);
// v5: onSuccess/onError are called at the call site (mutate), not here
return useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
authService.login(email, password),
});
};
// Usage in route file:
const loginMutation = useLogin();
loginMutation.mutate(
{ email, password },
{
onSuccess: (data) => login(data.token, data.user), // v5: callbacks here
onError: () => alert('Login failed'),
}
);
\`\`\``,
usage: `## Usage in a Route File
\`\`\`tsx
// app/(tabs)/catalog.tsx
import CatalogScreenUI from '@/screens/CatalogScreenUI';
import { useProducts } from '@/hooks/useProducts';
export default function CatalogRoute() {
const { data, isPending, error, refetch } = useProducts();
// v5: isPending (not isLoading) for queries without data yet
return (
<CatalogScreenUI
products={data?.items ?? []}
isLoading={isPending}
error={error?.message}
onRefresh={refetch}
/>
);
}
\`\`\``,
'v4-vs-v5': `## v4 vs v5 Key Differences
| Feature | v4 | v5 |
|---|---|---|
| Loading state | \`isLoading\` | \`isPending\` (for no-data state) |
| Cache eviction option | \`cacheTime\` | \`gcTime\` |
| Mutation callbacks | \`onSuccess\`/\`onError\` in \`useMutation({})\` | In \`mutate(data, { onSuccess, onError })\` |
| Query options object | separate \`useQuery(key, fn, options)\` | merged \`useQuery({ queryKey, queryFn, ...options })\` |`,
'query-keys': `## Query Key Conventions
Use a consistent key structure:
\`\`\`tsx
// Entity list
queryKey: ['products']
queryKey: ['products', { page: 1, category: 'electronics' }]
// Single entity
queryKey: ['product', productId]
// Nested entity
queryKey: ['product', productId, 'reviews']
// User-specific
queryKey: ['user', userId, 'orders']
\`\`\``,
'query-client-config': `## QueryClient Config
\`\`\`tsx
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes — data is considered fresh
gcTime: 1000 * 60 * 10, // 10 min before inactive cache is evicted (v5: gcTime, v4: cacheTime)
retry: 2, // 2 retries on error
refetchOnWindowFocus: false, // Don't refetch on focus (mobile app)
},
},
});
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
{/* ... */}
</QueryClientProvider>
);
}
\`\`\``,
rules: `## Rules
1. **Client state → Zustand**: auth, cart, preferences
2. **Server state → TanStack Query**: products, orders, profiles
3. **Custom hooks**: always create \`useProducts\`, \`useLogin\` — do not write \`useQuery\` directly in components
4. **Query keys**: consistent structure \`['entity', ...params]\`
5. **Error handling**: interceptor for 401, onError for specific errors
6. **Token injection**: via interceptor, do not pass token manually`,
};
// ─── Compact sections (rules only, no code) ─────────────────────────
const compactSections: Record<string, string> = {
'axios-client': `## Axios Client
- Create \`src/services/api/client.ts\` with \`axios.create({ baseURL, timeout, headers })\`
- Request interceptor: auto-inject auth token from \`useAuthStore.getState().token\`
- Response interceptor: auto-logout on 401`,
services: `## API Services
- Group by domain: \`src/services/api/auth.ts\`, \`products.ts\`, etc.
- Each service = object with async methods returning typed data
- Import \`apiClient\` from \`./client\``,
'query-hooks': `## Query Hooks
- ALWAYS create custom hooks (\`useProducts\`, \`useProduct\`) — never use \`useQuery\` in components directly
- Place in \`src/hooks/\`
- Use \`enabled\` option to conditionally fetch (e.g. \`enabled: !!id\`)`,
'mutation-hooks': `## Mutation Hooks
- Create \`useLogin\`, \`useCreateProduct\`, etc. wrapping \`useMutation\`
- v5: \`onSuccess\`/\`onError\` callbacks go in \`mutate(data, { onSuccess })\`, not in hook options`,
usage: `## Usage in Route
- Destructure: \`{ data, isPending, error, refetch }\` (v5: \`isPending\` not \`isLoading\`)
- Pass data to Screen UI via props`,
'v4-vs-v5': `## v4 vs v5
- \`isLoading\` → \`isPending\`, \`cacheTime\` → \`gcTime\`
- Mutation callbacks: in \`useMutation({})\` (v4) → in \`mutate(data, {})\` (v5)
- Query: separate args (v4) → single options object (v5)`,
'query-keys': `## Query Keys
- Entity list: \`['products']\`, with filters: \`['products', { page, category }]\`
- Single entity: \`['product', id]\`
- Nested: \`['product', id, 'reviews']\``,
'query-client-config': `## QueryClient Config
- \`staleTime\`: how long data is fresh (e.g. 5 min)
- \`gcTime\`: when inactive cache is evicted (e.g. 10 min)
- \`retry: 2\`, \`refetchOnWindowFocus: false\` for mobile`,
rules: `## Rules
- Client state → Zustand, Server state → TanStack Query
- Always custom hooks — never \`useQuery\` in components
- Query keys: \`['entity', ...params]\`
- Token injection via interceptor, not manually
- 401 handling via response interceptor`,
};
// ─── Export ──────────────────────────────────────────────────────────
const pattern: PatternSections = {
title: 'API Patterns (Axios + TanStack Query)',
sections,
compactSections,
};
export const getApiPatterns = (topic?: string, compact?: boolean): string =>
resolvePattern(pattern, topic, compact);