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-85
O 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.

Next Steps