API Integration
RNCopilot uses Axios as the HTTP client with two built-in interceptors for authentication and error handling. API calls are organized into service files and consumed through React Query hooks.
Axios Client
The client is configured in src/services/api/client.ts:
import axios from 'axios';
import { env } from '@/config/env';
import { useAuthStore } from '@/providers/auth/authStore';
const api = axios.create({
baseURL: env.apiBaseUrl,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
});Import it from the barrel:
import { api } from '@/services/api';Request Interceptor — Auto Bearer Token
Every outgoing request automatically attaches the auth token from the Zustand store:
api.interceptors.request.use((config) => {
const session = useAuthStore.getState().session;
if (session?.access_token) {
config.headers.Authorization = `Bearer ${session.access_token}`;
}
return config;
});You never need to manually pass authorization headers. If the user is authenticated, the token is attached. If not, the request goes without it.
Response Interceptor — Error Handling
The response interceptor handles two things:
1. Auto-Logout on 401
if (error.response?.status === 401) {
useAuthStore.getState().clearSession();
}When the server returns a 401 Unauthorized, the auth store is cleared automatically. If you have route protection via useProtectedRoute, the user is redirected to the login screen.
2. Error Normalization
const message =
error.response?.data?.message ||
error.response?.data?.error ||
error.message ||
'An unexpected error occurred';
return Promise.reject(new Error(message));All API errors are normalized to a standard Error object with a human-readable .message property. This means consuming code always catches Error, not AxiosError:
try {
await api.post('/products', data);
} catch (error) {
// error is always an Error with a .message string
console.error(error.message);
}The interceptor extracts the most useful error message from the response body (data.message or data.error), falling back to the Axios error message and finally a generic string. Your UI code never needs to parse API error shapes.
Service Files
API calls live in service files inside feature modules. Services are pure async functions — no React, no hooks, no state management:
// src/features/products/services/productService.ts
import { api } from '@/services/api';
import type { Product, CreateProductData } from '../types';
export async function fetchProducts(): Promise<Product[]> {
const { data } = await api.get<Product[]>('/products');
return data;
}
export async function fetchProduct(id: string): Promise<Product> {
const { data } = await api.get<Product>(`/products/${id}`);
return data;
}
export async function createProduct(
payload: CreateProductData
): Promise<Product> {
const { data } = await api.post<Product>('/products', payload);
return data;
}
export async function updateProduct(
id: string,
payload: Partial<CreateProductData>
): Promise<Product> {
const { data } = await api.patch<Product>(`/products/${id}`, payload);
return data;
}
export async function deleteProduct(id: string): Promise<void> {
await api.delete(`/products/${id}`);
}React Query Hooks
Service functions are consumed through React Query hooks, which provide caching, loading states, error handling, and automatic refetching:
Query Key Factory
// src/features/products/hooks/useProducts.ts
export const PRODUCT_KEYS = {
all: ['products'] as const,
list: (filters?: object) =>
[...PRODUCT_KEYS.all, 'list', filters] as const,
detail: (id: string) =>
[...PRODUCT_KEYS.all, 'detail', id] as const,
};Use factory functions for query keys, not inline arrays. This enables precise cache invalidation — invalidateQueries({ queryKey: PRODUCT_KEYS.all }) invalidates both lists and details.
Query Hook (Fetching Data)
import { useQuery } from '@tanstack/react-query';
import { fetchProducts } from '../services/productService';
export function useProducts(filters?: object) {
return useQuery({
queryKey: PRODUCT_KEYS.list(filters),
queryFn: () => fetchProducts(filters),
});
}
export function useProduct(id: string) {
return useQuery({
queryKey: PRODUCT_KEYS.detail(id),
queryFn: () => fetchProduct(id),
enabled: !!id, // Don't fetch if id is empty
});
}Mutation Hook (Creating/Updating/Deleting)
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createProduct, deleteProduct } from '../services/productService';
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateProductData) => createProduct(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: PRODUCT_KEYS.all,
});
},
});
}
export function useDeleteProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteProduct(id),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: PRODUCT_KEYS.all,
});
},
});
}Using Hooks in Components
import { FlatList } from 'react-native';
import { useTranslation } from 'react-i18next';
import { ScreenContainer } from '@/common/components/ScreenContainer';
import { EmptyState } from '@/common/components/EmptyState';
import { Loading } from '@/common/components/Loading';
import { useProducts } from '@/features/products/hooks/useProducts';
export default function ProductsScreen() {
const { t } = useTranslation();
const { data: products, isLoading, error, refetch } = useProducts();
if (isLoading) return <Loading fullScreen />;
if (error) {
return (
<EmptyState
title={t('errors.generic')}
message={error.message}
actionLabel={t('common.retry')}
onAction={refetch}
/>
);
}
return (
<ScreenContainer>
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ProductCard product={item} />}
ListEmptyComponent={
<EmptyState title={t('products.empty')} />
}
/>
</ScreenContainer>
);
}Architecture Summary
Component
|
+-- React Query Hook (useProducts)
|
+-- Service Function (fetchProducts)
|
+-- Axios Client (api.get)
|
+-- Request Interceptor (attach token)
+-- Response Interceptor (handle errors)Never put API calls directly in components. Always go through a service function and a React Query hook. This keeps components focused on rendering and makes API logic testable and reusable.