API Integration Guide
Learn how to integrate external APIs in your app using Axios and React Query.
Quick Start
Follow these steps to add a new API endpoint:
1. Define Entity Type
src/domain/entities/post.ts
export interface Post {
id: string;
title: string;
body: string;
userId: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreatePostInput {
title: string;
body: string;
}
export interface UpdatePostInput extends Partial<CreatePostInput> {
id: string;
}2. Create API Endpoint
src/data/api/endpoints/posts.ts
import { getHttpClient } from '@/core/config';
import type { Post, CreatePostInput, UpdatePostInput } from '@/domain/entities/post';
export const postApi = {
// GET /posts
getPosts: async (): Promise<Post[]> => {
const httpClient = getHttpClient();
const { data } = await httpClient.get<Post[]>('/posts');
return data;
},
// GET /posts/:id
getPost: async (id: string): Promise<Post> => {
const httpClient = getHttpClient();
const { data } = await httpClient.get<Post>(`/posts/${id}`);
return data;
},
// POST /posts
createPost: async (input: CreatePostInput): Promise<Post> => {
const httpClient = getHttpClient();
const { data } = await httpClient.post<Post, CreatePostInput>('/posts', input);
return data;
},
// PUT /posts/:id
updatePost: async ({ id, ...input }: UpdatePostInput): Promise<Post> => {
const httpClient = getHttpClient();
const { data } = await httpClient.put<Post, Partial<CreatePostInput>>(`/posts/${id}`, input);
return data;
},
// DELETE /posts/:id
deletePost: async (id: string): Promise<void> => {
const httpClient = getHttpClient();
await httpClient.delete<void>(`/posts/${id}`);
},
};3. Create ViewModel
src/presentation/viewmodels/usePostListViewModel.ts
import { useApiQuery, useApiMutation } from '@/hooks/useApi';
import { useQueryClient } from '@tanstack/react-query';
import { postApi } from '@/data/api/endpoints/posts';
import { router } from 'expo-router';
import type { Post } from '@/domain/entities/post';
export function usePostListViewModel() {
const queryClient = useQueryClient();
// Fetch posts using useApiQuery
const { data, isLoading, error, refetch } = useApiQuery<Post[]>(
['posts'],
'/posts'
);
// Delete post using useApiMutation
const deleteMutation = useApiMutation<void, string>(
'delete',
(id) => `/posts/${id}`,
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
}
);
const handleDelete = (id: string) => {
deleteMutation.mutate(id);
};
const handleCreateNew = () => {
router.push('/posts/new');
};
return {
posts: data?.data ?? [],
isLoading,
error,
refetch,
handleDelete,
handleCreateNew,
isDeleting: deleteMutation.isPending,
};
}4. Use in View
typescript
import { View, Text, FlatList, Pressable, ActivityIndicator } from 'react-native';
import { usePostListViewModel } from '@/presentation/viewmodels/usePostListViewModel';
export default function PostListScreen() {
const { posts, isLoading, error, handleDelete } = usePostListViewModel();
if (isLoading) {
return <ActivityIndicator size="large" />;
}
if (error) {
return <Text>Error: {error.message}</Text>;
}
return (
<FlatList
data={posts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View className="bg-white dark:bg-slate-800 p-4 mb-2 rounded-lg">
<Text className="font-semibold text-lg mb-1">{item.title}</Text>
<Text className="text-slate-600 dark:text-slate-400">{item.body}</Text>
<Pressable onPress={() => handleDelete(item.id)}>
<Text className="text-red-500 mt-2">Delete</Text>
</Pressable>
</View>
)}
/>
);
}Authentication
The boilerplate already includes authentication handling in the HTTP client:
src/data/api/axios.client.ts
import { createHttpClient } from '@/data/api';
import { getStorage } from '@/core/config';
// O httpClient já é configurado no bootstrap com interceptors:
// 1. Request interceptor - adiciona token automaticamente
// 2. Response interceptor - trata 401 e refresh token
// Para definir o token após login:
const httpClient = getHttpClient();
httpClient.setAuthToken('your-jwt-token');
// Para remover o token no logout:
httpClient.setAuthToken(null);
// O token é automaticamente adicionado em todas as requisições:
// Authorization: Bearer <token>
// Refresh token (implementar no interceptor):
// Se receber 401, o interceptor tenta refresh automaticamente
// Ver src/data/api/axios.client.ts linha 70-85O cliente HTTP já inclui interceptors configurados. Tokens são salvos no MMKV e adicionados automaticamente às requisições.
Error Handling
typescript
// O httpClient normaliza erros automaticamente
// Todos os erros do Axios são convertidos para Error com status
try {
const httpClient = getHttpClient();
const { data } = await httpClient.get<Post[]>('/posts');
return data;
} catch (error) {
// Error já vem normalizado do httpClient
const err = error as Error & { status?: number };
if (err.status === 404) {
console.log('Post não encontrado');
} else if (err.status === 401) {
console.log('Não autenticado');
} else {
console.log('Erro:', err.message);
}
}
// Em componentes com React Query:
const { error } = useApiQuery<Post[]>(['posts'], '/posts');
if (error) {
return <Text>Erro: {error.message}</Text>;
}Pagination
typescript
export function useInfinitePostsViewModel() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) =>
postApi.getPosts({ page: pageParam, limit: 20 }),
getNextPageParam: (lastPage, pages) =>
lastPage.hasMore ? pages.length + 1 : undefined,
initialPageParam: 1,
});
const posts = data?.pages.flatMap(page => page.data) ?? [];
return {
posts,
loadMore: fetchNextPage,
hasMore: hasNextPage,
isLoadingMore: isFetchingNextPage,
};
}File Upload
typescript
import * as ImagePicker from 'expo-image-picker';
import { getHttpClient } from '@/core/config';
import { useApiMutation } from '@/hooks/useApi';
export const uploadApi = {
uploadImage: async (uri: string): Promise<{ url: string }> => {
const httpClient = getHttpClient();
const formData = new FormData();
// @ts-ignore - FormData types
formData.append('file', {
uri,
type: 'image/jpeg',
name: 'upload.jpg',
});
const { data } = await httpClient.post<{ url: string }, FormData>(
'/upload',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
return data;
},
};
// In ViewModel
export function useImageUploadViewModel() {
const uploadMutation = useApiMutation<{ url: string }, string>(
'post',
'/upload',
{
onSuccess: (response) => {
console.log('Image uploaded:', response.data.url);
},
}
);
const handlePickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
});
if (!result.canceled) {
uploadMutation.mutate(result.assets[0].uri);
}
};
return {
handlePickImage,
isUploading: uploadMutation.isPending,
};
}Always handle loading and error states in your UI for better user experience.