Skip to Content
Core ConceptsAPI Integration

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.

Last updated on