28. Obtención de datos desde una API

 

Tutorial: Obtención de Datos desde una API en React Native

🌐 Introducción a las APIs REST

En el desarrollo de aplicaciones móviles modernas, es esencial conectar con servidores en la nube para obtener, enviar y manipular datos. Hoy aprenderemos a consumir APIs REST usando dos métodos populares en React Native.

API Utilizada: Rick and Morty API

  • API pública y gratuita

  • Soporta REST y GraphQL

  • Contiene datos de personajes, locaciones y episodios


🛠️ Paso 1: Configuración del Proyecto

Estructura de carpetas:

text

src/

├── api/

│   ├── RickAndMortyApi.ts      # Método con Fetch

│   ├── RickAndMortyApiAlt.ts   # Método con Axios

│   └── entities/               # Interfaces TypeScript

│       ├── ApiInfoResponse.ts

│       └── CharacterResponse.ts

└── screens/

    └── Profile.tsx

Instalación de dependencias:

bash

# Para usar Axios necesitamos instalarlo

npm install axios

# o

yarn add axios


🔄 Paso 2: Método 1 - Usando Fetch (Nativo de React Native)

RickAndMortyApi.ts:

typescript

import { ApiInfoResponse } from './entities/ApiInfoResponse';


/**

 * Obtiene información general de la API usando Fetch

 * @returns Promise con la respuesta de la API

 */

export const getApiInfo = (): Promise<Response> => {

  return fetch('https://rickandmortyapi.com/api', {

    method: 'GET',

    // Podemos agregar headers si es necesario

    headers: {

      'Content-Type': 'application/json',

    },

  });

};


/**

 * Obtiene personajes usando Fetch (alternativa)

 */

export const getCharactersWithFetch = async (page: number = 1): Promise<any> => {

  try {

    const response = await fetch(

      `https://rickandmortyapi.com/api/character?page=${page}`,

      {

        method: 'GET',

        headers: {

          'Content-Type': 'application/json',

        },

      }

    );

    

    if (!response.ok) {

      throw new Error(`Error: ${response.status}`);

    }

    

    return await response.json();

  } catch (error) {

    console.error('Error fetching characters:', error);

    throw error;

  }

};


📦 Paso 3: Método 2 - Usando Axios (Librería Externa)

RickAndMortyApiAlt.ts:

typescript

import axios from 'axios';

import { CharacterResponse } from './entities/CharacterResponse';


// Crear instancia de Axios con configuración base

const apiClient = axios.create({

  baseURL: 'https://rickandmortyapi.com/api',

  timeout: 10000, // 10 segundos timeout

  headers: {

    'Content-Type': 'application/json',

  },

});


/**

 * Obtiene personajes usando Axios

 * @param page Número de página (la API está paginada)

 * @returns Promise con los personajes

 */

export const getCharacters = async (

  page: number = 1

): Promise<CharacterResponse> => {

  try {

    const response = await apiClient.get<CharacterResponse>(

      `/character?page=${page}`

    );

    return response.data;

  } catch (error) {

    console.error('Error fetching characters with Axios:', error);

    throw error;

  }

};


/**

 * Obtiene información de un personaje específico

 */

export const getCharacterById = async (

  id: number

): Promise<Character> => {

  const response = await apiClient.get<Character>(

    `/character/${id}`

  );

  return response.data;

};


📝 Paso 4: Definir Interfaces TypeScript

entities/ApiInfoResponse.ts:

typescript

/**

 * Interfaz para la respuesta de información de la API

 */

export interface ApiInfoResponse {

  characters: string;   // URL del endpoint de personajes

  locations: string;    // URL del endpoint de locaciones

  episodes: string;     // URL del endpoint de episodios

}

entities/CharacterResponse.ts:

typescript

/**

 * Interfaz para la información de paginación

 */

export interface Info {

  count: number;    // Total de personajes (826)

  pages: number;    // Total de páginas (42)

  next: string | null;     // URL de siguiente página

  prev: string | null;     // URL de página anterior

}


/**

 * Interfaz para un personaje individual

 */

export interface Character {

  id: number;

  name: string;

  status: 'Alive' | 'Dead' | 'unknown';

  species: string;

  type: string;

  gender: 'Female' | 'Male' | 'Genderless' | 'unknown';

  origin: {

    name: string;

    url: string;

  };

  location: {

    name: string;

    url: string;

  };

  image: string;    // URL de la imagen

  episode: string[]; // URLs de episodios

  url: string;

  created: string;  // Fecha de creación

}


/**

 * Interfaz para la respuesta completa de personajes

 */

export interface CharacterResponse {

  info: Info;

  results: Character[];

}


🎯 Paso 5: Consumir APIs en el Componente Principal

App.tsx completo:

typescript

import React, { useEffect, useState } from 'react';

import { SafeAreaView, View, Text, ActivityIndicator, StyleSheet } from 'react-native';

import { getApiInfo } from './src/api/RickAndMortyApi';

import { ApiInfoResponse } from './src/api/entities/ApiInfoResponse';

import { getCharacters } from './src/api/RickAndMortyApiAlt';

import { CharacterResponse } from './src/api/entities/CharacterResponse';


function App(): JSX.Element {

  const [apiInfo, setApiInfo] = useState<ApiInfoResponse | null>(null);

  const [characters, setCharacters] = useState<CharacterResponse | null>(null);

  const [loading, setLoading] = useState<boolean>(true);

  const [error, setError] = useState<string | null>(null);


  // Método 1: Usando Fetch con .then() y .catch()

  useEffect(() => {

    getApiInfo()

      .then(response => response.json())

      .then((data: ApiInfoResponse) => {

        console.log('ApiInfo:', JSON.stringify(data));

        setApiInfo(data);

      })

      .catch((error: Error) => {

        console.error('Error fetching API info:', error);

        setError('Error al obtener información de la API');

      });

  }, []);


  // Método 2: Usando Axios con async/await

  useEffect(() => {

    const fetchCharacters = async () => {

      try {

        setLoading(true);

        const response = await getCharacters();

        console.log('Characters Info:', JSON.stringify(response.info));

        setCharacters(response);

      } catch (error) {

        console.error('Error fetching characters:', error);

        setError('Error al obtener personajes');

      } finally {

        setLoading(false);

      }

    };


    fetchCharacters();

  }, []);


  if (loading) {

    return (

      <SafeAreaView style={styles.container}>

        <ActivityIndicator size="large" color="#0000ff" />

        <Text>Cargando datos...</Text>

      </SafeAreaView>

    );

  }


  if (error) {

    return (

      <SafeAreaView style={styles.container}>

        <Text style={styles.errorText}>{error}</Text>

      </SafeAreaView>

    );

  }


  return (

    <SafeAreaView style={styles.container}>

      <View style={styles.section}>

        <Text style={styles.title}>Información de la API</Text>

        {apiInfo && (

          <>

            <Text>Personajes: {apiInfo.characters}</Text>

            <Text>Locaciones: {apiInfo.locations}</Text>

            <Text>Episodios: {apiInfo.episodes}</Text>

          </>

        )}

      </View>


      <View style={styles.section}>

        <Text style={styles.title}>Personajes</Text>

        {characters && (

          <>

            <Text>Total: {characters.info.count} personajes</Text>

            <Text>Páginas: {characters.info.pages}</Text>

            <Text>Personajes en esta página: {characters.results.length}</Text>

            

            {/* Mostrar primeros 3 personajes como ejemplo */}

            {characters.results.slice(0, 3).map(character => (

              <View key={character.id} style={styles.characterCard}>

                <Text style={styles.characterName}>{character.name}</Text>

                <Text>Estado: {character.status}</Text>

                <Text>Especie: {character.species}</Text>

              </View>

            ))}

          </>

        )}

      </View>

    </SafeAreaView>

  );

}


const styles = StyleSheet.create({

  container: {

    flex: 1,

    padding: 16,

    backgroundColor: '#f5f5f5',

  },

  section: {

    backgroundColor: 'white',

    padding: 16,

    marginBottom: 16,

    borderRadius: 8,

    shadowColor: '#000',

    shadowOffset: { width: 0, height: 2 },

    shadowOpacity: 0.1,

    shadowRadius: 4,

    elevation: 3,

  },

  title: {

    fontSize: 18,

    fontWeight: 'bold',

    marginBottom: 12,

    color: '#333',

  },

  characterCard: {

    backgroundColor: '#f9f9f9',

    padding: 12,

    marginBottom: 8,

    borderRadius: 6,

    borderWidth: 1,

    borderColor: '#e0e0e0',

  },

  characterName: {

    fontSize: 16,

    fontWeight: '600',

    color: '#2196F3',

  },

  errorText: {

    color: 'red',

    fontSize: 16,

    textAlign: 'center',

  },

});


export default App;


🔍 Paso 6: Comparación Fetch vs Axios

Fetch (Nativo):

typescript

// Ventajas:

// ✅ Viene incluido en React Native

// ✅ No requiere dependencias adicionales

// ✅ API estándar de JavaScript


// Desventajas:

// ❌ Necesita conversión manual a JSON

// ❌ Manejo de errores menos intuitivo

// ❌ No tiene cancelación de requests por defecto


const response = await fetch(url);

const data = await response.json();

Axios (Librería):

typescript

// Ventajas:

// ✅ Conversión automática a JSON

// ✅ Manejo de errores más robusto

// ✅ Cancelación de requests

// ✅ Interceptores para headers globales

// ✅ Transformadores de request/response


// Desventajas:

// ❌ Dependencia externa

// ❌ Aumenta tamaño del bundle


const response = await axios.get(url);

const data = response.data;


🛡️ Paso 7: Manejo de Errores Avanzado

RickAndMortyApiAlt.ts con manejo de errores mejorado:

typescript

import axios, { AxiosError } from 'axios';


// Interceptor para agregar headers globales

apiClient.interceptors.request.use(

  (config) => {

    // Podemos agregar tokens de autenticación aquí

    // config.headers.Authorization = `Bearer ${token}`;

    return config;

  },

  (error) => {

    return Promise.reject(error);

  }

);


// Interceptor para respuestas

apiClient.interceptors.response.use(

  (response) => response,

  (error: AxiosError) => {

    if (error.response) {

      // El servidor respondió con un código de error

      switch (error.response.status) {

        case 401:

          console.error('No autorizado - Token expirado');

          break;

        case 403:

          console.error('Prohibido - Sin permisos');

          break;

        case 404:

          console.error('Recurso no encontrado');

          break;

        case 500:

          console.error('Error interno del servidor');

          break;

        default:

          console.error(`Error ${error.response.status}:`, error.message);

      }

    } else if (error.request) {

      // La petición fue hecha pero no hubo respuesta

      console.error('Sin respuesta del servidor - Revisa tu conexión');

    } else {

      // Error al configurar la petición

      console.error('Error en la configuración:', error.message);

    }

    

    return Promise.reject(error);

  }

);


📱 Paso 8: Custom Hook para Fetching de Datos

useCharacters.ts:

typescript

import { useState, useEffect, useCallback } from 'react';

import { getCharacters } from '../api/RickAndMortyApiAlt';

import { CharacterResponse } from '../api/entities/CharacterResponse';


export const useCharacters = (initialPage: number = 1) => {

  const [data, setData] = useState<CharacterResponse | null>(null);

  const [loading, setLoading] = useState<boolean>(false);

  const [error, setError] = useState<string | null>(null);

  const [page, setPage] = useState<number>(initialPage);


  const fetchCharacters = useCallback(async (pageNum: number) => {

    try {

      setLoading(true);

      setError(null);

      const response = await getCharacters(pageNum);

      setData(response);

    } catch (err) {

      setError(err instanceof Error ? err.message : 'Error desconocido');

    } finally {

      setLoading(false);

    }

  }, []);


  useEffect(() => {

    fetchCharacters(page);

  }, [page, fetchCharacters]);


  const nextPage = () => {

    if (data?.info.next) {

      setPage(prev => prev + 1);

    }

  };


  const prevPage = () => {

    if (data?.info.prev) {

      setPage(prev => prev - 1);

    }

  };


  return {

    data,

    loading,

    error,

    page,

    nextPage,

    prevPage,

    refetch: () => fetchCharacters(page),

  };

};

Uso en componente:

typescript

const CharacterList = () => {

  const { data, loading, error, page, nextPage, prevPage } = useCharacters();


  if (loading) return <Text>Cargando...</Text>;

  if (error) return <Text>Error: {error}</Text>;


  return (

    <View>

      <Text>Página {page} de {data?.info.pages}</Text>

      <Button title="Anterior" onPress={prevPage} disabled={!data?.info.prev} />

      <Button title="Siguiente" onPress={nextPage} disabled={!data?.info.next} />

      

      {data?.results.map(character => (

        <Text key={character.id}>{character.name}</Text>

      ))}

    </View>

  );

};


💡 Mejores Prácticas

1. Separación de responsabilidades:

  • ✅ APIs en archivos separados

  • ✅ Interfaces TypeScript para tipos de datos

  • ✅ Custom hooks para lógica reutilizable

2. Manejo de estados:

typescript

const [data, setData] = useState<T | null>(null);

const [loading, setLoading] = useState<boolean>(true);

const [error, setError] = useState<string | null>(null);

3. Limpieza de efectos:

typescript

useEffect(() => {

  let isMounted = true;

  const controller = new AbortController();


  const fetchData = async () => {

    try {

      const response = await fetch(url, { 

        signal: controller.signal 

      });

      if (isMounted) {

        setData(await response.json());

      }

    } catch (err) {

      if (isMounted && err.name !== 'AbortError') {

        setError(err.message);

      }

    }

  };


  fetchData();


  return () => {

    isMounted = false;

    controller.abort();

  };

}, []);

4. Variables de entorno:

typescript

// .env

API_URL=https://rickandmortyapi.com/api


// En el código

const API_URL = process.env.API_URL;


🎓 Resumen

Lo que aprendiste:

✅ Consumir APIs REST con Fetch (nativo)
✅ Consumir APIs REST con Axios (librería externa)
✅ Definir interfaces TypeScript para respuestas
✅ Manejar estados de carga y error
✅ Implementar paginación
✅ Crear custom hooks para reutilizar lógica
✅ Manejo de errores robusto

Siguientes pasos:

  1. Mostrar datos en una lista con FlatList

  2. Implementar búsqueda en tiempo real

  3. Cachear respuestas para mejor performance

  4. Agregar pull-to-refresh

  5. Implementar infinite scroll


📚 Recursos Adicionales

Documentación oficial:

Herramientas útiles:

¡Ahora estás listo para crear aplicaciones que consumen datos de APIs reales!


Comentarios

Entradas más populares de este blog

Guía Paso a Paso para Entender React Native (antes del Tutorial)

Tutorial: Aplicación React Native para Agregar Tareas - Minimalista

5. Vista Rapida: Estructura Projecto Base