Ir al contenido principal

Consumo de APIs externas

Introducción

En la actualidad, las aplicaciones web rara vez funcionan de forma aislada. La mayoría depende de servicios externos para obtener datos, realizar operaciones o integrar funcionalidades. Estos servicios se exponen a través de APIs (Application Programming Interfaces), que permiten a diferentes sistemas comunicarse entre sí mediante un conjunto de reglas bien definidas.

En nuestro artículo anterior exploramos los métodos HTTP y su funcionamiento. Ahora daremos un paso más allá y aprenderemos cómo consumir APIs externas desde JavaScript, lo que nos permitirá ampliar enormemente las capacidades de nuestras aplicaciones sin tener que desarrollar toda esa funcionalidad por nuestra cuenta.

Conceptos básicos de APIs web

Una API web es una interfaz que permite a diferentes aplicaciones comunicarse entre sí a través de Internet utilizando el protocolo HTTP. Podemos visualizarla como un "contrato" entre un proveedor de servicios y sus consumidores, donde se definen las operaciones disponibles, el formato de los datos y las reglas de comunicación.

Tipos comunes de APIs web

  1. APIs RESTful: Utilizan los métodos HTTP estándar (GET, POST, PUT, DELETE) y URLs para representar recursos. Son las más comunes actualmente.

  2. APIs SOAP: Utilizan XML y un protocolo más rígido. Son más antiguas pero aún se utilizan en entornos empresariales.

  3. APIs GraphQL: Permiten a los clientes solicitar exactamente los datos que necesitan, minimizando las transferencias de datos innecesarias.

  4. APIs WebSocket: Proporcionan comunicación bidireccional en tiempo real.

En este artículo nos centraremos principalmente en las APIs RESTful, ya que son las más extendidas.

Recursos y endpoints

En una API RESTful, los recursos (como usuarios, productos, artículos) se identifican mediante URLs llamadas endpoints. Por ejemplo:

https://api.ejemplo.com/usuarios       // Colección de usuarios
https://api.ejemplo.com/usuarios/123   // Usuario específico con ID 123
https://api.ejemplo.com/productos      // Colección de productos

Formato de datos

La mayoría de las APIs modernas utilizan JSON (JavaScript Object Notation) como formato para intercambiar datos. JSON es ligero, fácil de leer y escribir, y puede ser parseado directamente en objetos JavaScript.

// Ejemplo de datos en formato JSON
{
  "id": 123,
  "nombre": "Ana García",
  "email": "ana@ejemplo.com",
  "activo": true,
  "roles": ["editor", "admin"]
}

Algunas APIs más antiguas pueden utilizar XML, pero su uso está disminuyendo en favor de JSON.

Autenticación y autorización

La mayoría de las APIs requieren algún tipo de autenticación para controlar el acceso a sus recursos. Existen varios métodos comunes:

API Keys

Un método simple donde se proporciona una clave única en cada solicitud, generalmente como parámetro de consulta o en un encabezado HTTP.

fetch('https://api.ejemplo.com/datos?api_key=tu_clave_api')
  .then(respuesta => respuesta.json())
  .then(datos => console.log(datos));

// O usando headers
fetch('https://api.ejemplo.com/datos', {
  headers: {
    'X-API-Key': 'tu_clave_api'
  }
})
  .then(respuesta => respuesta.json())
  .then(datos => console.log(datos));

Autenticación básica

Utiliza el esquema "Basic" de HTTP enviando el nombre de usuario y contraseña codificados en Base64.

const credenciales = btoa('usuario:contraseña'); // Codifica en Base64

fetch('https://api.ejemplo.com/datos', {
  headers: {
    'Authorization': `Basic ${credenciales}`
  }
})
  .then(respuesta => respuesta.json())
  .then(datos => console.log(datos));

Tokens JWT (JSON Web Tokens)

Un enfoque más moderno y seguro donde el servidor emite un token después de la autenticación inicial, y este token se utiliza para solicitudes posteriores.

// Primero obtener el token mediante login
fetch('https://api.ejemplo.com/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    usuario: 'miusuario',
    contraseña: 'micontraseña'
  })
})
  .then(respuesta => respuesta.json())
  .then(datos => {
    // Guardar el token
    const token = datos.token;
    
    // Usar el token en solicitudes posteriores
    return fetch('https://api.ejemplo.com/datos-protegidos', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
  })
  .then(respuesta => respuesta.json())
  .then(datosProtegidos => console.log(datosProtegidos));

OAuth 2.0

Un protocolo de autorización complejo pero potente que permite a las aplicaciones obtener acceso limitado a cuentas de usuario en otros servicios sin compartir credenciales. Es utilizado por grandes servicios como Google, Facebook, Twitter, etc.

La implementación completa de OAuth está fuera del alcance de este artículo, pero generalmente implica estos pasos:

  1. Registrar tu aplicación con el proveedor del servicio
  2. Redirigir al usuario al servicio para autorizar
  3. Recibir un código de autorización
  4. Intercambiar el código por un token de acceso
  5. Usar el token para acceder a los recursos

Manejo de tokens

Cuando trabajamos con tokens de autenticación, es importante manejarlos correctamente:

Almacenamiento de tokens

Podemos almacenar tokens temporalmente en el navegador usando:

// Guardar token en localStorage (persiste incluso después de cerrar el navegador)
localStorage.setItem('authToken', token);

// Recuperar token
const token = localStorage.setItem('authToken');

// O en sessionStorage (se borra al cerrar la pestaña)
sessionStorage.setItem('authToken', token);

Nota de seguridad: localStorage y sessionStorage son vulnerables a ataques XSS. Para aplicaciones con requisitos de seguridad elevados, considera usar cookies HttpOnly.

Renovación de tokens

Muchas APIs emiten tokens con un tiempo de vida limitado. Cuando un token expira, debemos renovarlo:

const verificarToken = async () => {
  const token = localStorage.getItem('authToken');
  
  // Verificar si el token está próximo a expirar
  if (tokenEstaExpirado(token)) {
    try {
      // Solicitar un nuevo token usando un refresh token
      const respuesta = await fetch('https://api.ejemplo.com/refresh-token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          refreshToken: localStorage.getItem('refreshToken')
        })
      });
      
      const datos = await respuesta.json();
      
      // Guardar el nuevo token
      localStorage.setItem('authToken', datos.newToken);
      
      return datos.newToken;
    } catch (error) {
      console.error('Error al renovar token:', error);
      // Si falla la renovación, redirigir al login
      window.location.href = '/login';
    }
  }
  
  return token;
};

// Función simple para comprobar expiración
// (en la práctica, los JWT tienen una fecha de expiración que se puede decodificar)
const tokenEstaExpirado = (token) => {
  // Implementación simple para ejemplo
  // En producción, decodificarías el token y verificarías su fecha de expiración
  return false;
};

CORS y problemas de seguridad

CORS (Cross-Origin Resource Sharing) es un mecanismo de seguridad implementado por los navegadores que restringe las solicitudes HTTP realizadas desde un origen (dominio) a otro. Esto previene que scripts maliciosos accedan a datos de otros sitios.

Qué es un error CORS

Si intentas acceder a una API desde un dominio diferente y la API no tiene habilitado CORS correctamente, verás un error como este en la consola:

Access to fetch at 'https://api.ejemplo.com/datos' from origin 'https://miapp.com' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on 
the requested resource.

Soluciones a problemas CORS

  1. Desde el servidor: La solución ideal es que el servidor de la API configure correctamente los encabezados CORS:
Access-Control-Allow-Origin: https://miapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
  1. Proxy de desarrollo: Durante el desarrollo, puedes configurar un servidor proxy en tu entorno local que redireccione las solicitudes a la API externa. Muchos frameworks como React (create-react-app), Vue (Vue CLI) o herramientas como Webpack Dev Server tienen opciones de configuración para esto.

  2. Proxy de servidor: En producción, puedes configurar tu servidor para que actúe como intermediario entre tu frontend y la API externa.

Importante: CORS es una restricción del navegador. Las solicitudes desde un servidor Node.js, por ejemplo, no están sujetas a estas restricciones.

Formatos de datos (JSON, XML)

Como mencionamos anteriormente, JSON es el formato más común en las APIs modernas debido a su simplicidad y compatibilidad directa con JavaScript.

Trabajando con JSON

JavaScript tiene métodos nativos para trabajar con JSON:

// Conversión de objeto JavaScript a string JSON
const usuario = {
  nombre: 'Carlos',
  edad: 28,
  roles: ['admin', 'editor']
};

const usuarioJSON = JSON.stringify(usuario);
console.log(usuarioJSON);
// Resultado: '{"nombre":"Carlos","edad":28,"roles":["admin","editor"]}'

// Conversión de string JSON a objeto JavaScript
const usuarioObjeto = JSON.parse(usuarioJSON);
console.log(usuarioObjeto.nombre); // 'Carlos'

Trabajando con XML

Si necesitas trabajar con una API que utiliza XML, JavaScript proporciona el objeto DOMParser para analizar XML:

const xmlString = `
<usuario>
  <nombre>Carlos</nombre>
  <edad>28</edad>
  <roles>
    <rol>admin</rol>
    <rol>editor</rol>
  </roles>
</usuario>
`;

const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "text/xml");

// Acceder a los datos
const nombre = xmlDoc.getElementsByTagName("nombre")[0].textContent;
const edad = xmlDoc.getElementsByTagName("edad")[0].textContent;

console.log(nombre); // 'Carlos'
console.log(edad);   // '28'

// Obtener todos los roles
const roles = xmlDoc.getElementsByTagName("rol");
for (let i = 0; i < roles.length; i++) {
  console.log(roles[i].textContent);
}
// Resultado: 'admin', 'editor'

Rate limiting y optimización

Las APIs suelen tener límites en cuanto al número de solicitudes que se pueden hacer en un período determinado. Esto se conoce como "rate limiting" y es importante tenerlo en cuenta al diseñar aplicaciones.

Buenas prácticas para manejar límites de tasa

  1. Caché de respuestas: Almacena temporalmente los resultados de solicitudes frecuentes.
const cache = new Map();

const fetchConCache = async (url, opciones = {}) => {
  // Usar una clave para el caché que incluya la URL y cualquier parámetro relevante
  const cacheKey = url + JSON.stringify(opciones);
  
  // Verificar si tenemos una respuesta en caché y si aún es válida
  if (cache.has(cacheKey)) {
    const { data, timestamp } = cache.get(cacheKey);
    // Si la caché tiene menos de 5 minutos, usar los datos en caché
    if (Date.now() - timestamp < 5 * 60 * 1000) {
      return data;
    }
  }
  
  // Si no hay caché válida, hacer la solicitud
  try {
    const respuesta = await fetch(url, opciones);
    const datos = await respuesta.json();
    
    // Guardar en caché junto con la marca de tiempo
    cache.set(cacheKey, {
      data: datos,
      timestamp: Date.now()
    });
    
    return datos;
  } catch (error) {
    console.error('Error en fetchConCache:', error);
    throw error;
  }
};

// Uso
fetchConCache('https://api.ejemplo.com/datos')
  .then(datos => console.log(datos));
  1. Agrupamiento de solicitudes: Combina múltiples operaciones en una sola solicitud cuando sea posible.

  2. Implementación de retardo: Si necesitas hacer muchas solicitudes, añade un retardo entre ellas.

const esperarMs = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const hacerSolicitudesSecuenciales = async (urls) => {
  const resultados = [];
  
  for (const url of urls) {
    try {
      // Hacer la solicitud
      const respuesta = await fetch(url);
      const datos = await respuesta.json();
      resultados.push(datos);
      
      // Esperar 1 segundo antes de la siguiente solicitud
      await esperarMs(1000);
    } catch (error) {
      console.error(`Error al solicitar ${url}:`, error);
      resultados.push(null); // O manejar el error como prefieras
    }
  }
  
  return resultados;
};

// Uso
const urls = [
  'https://api.ejemplo.com/datos/1',
  'https://api.ejemplo.com/datos/2',
  'https://api.ejemplo.com/datos/3'
];

hacerSolicitudesSecuenciales(urls)
  .then(resultados => console.log(resultados));
  1. Monitoreo de límites: Muchas APIs incluyen encabezados que indican tus límites actuales y cuántas solicitudes te quedan. Monitorea estos encabezados para evitar exceder los límites.
fetch('https://api.ejemplo.com/datos')
  .then(respuesta => {
    // Verificar encabezados de límite (los nombres varían según la API)
    const limiteRateLimit = respuesta.headers.get('X-RateLimit-Limit');
    const restanteRateLimit = respuesta.headers.get('X-RateLimit-Remaining');
    const resetRateLimit = respuesta.headers.get('X-RateLimit-Reset');
    
    console.log(`Límite: ${limiteRateLimit}, Restante: ${restanteRateLimit}`);
    
    // Si estamos cerca del límite, podríamos implementar un retardo
    if (restanteRateLimit < 10) {
      console.warn('¡Cerca del límite de solicitudes!');
    }
    
    return respuesta.json();
  })
  .then(datos => console.log(datos));

Documentación y versionado de APIs

Uno de los primeros pasos al trabajar con una API externa es consultar su documentación, que suele incluir:

  • Endpoints disponibles
  • Métodos HTTP soportados
  • Parámetros requeridos y opcionales
  • Formato de las respuestas
  • Códigos de error
  • Límites de tasa
  • Ejemplos de uso

Versionado de APIs

Las APIs evolucionan con el tiempo, y los cambios podrían romper las aplicaciones existentes. Por eso, muchas APIs implementan versionado, generalmente de estas formas:

  1. En la URL: https://api.ejemplo.com/v1/recursos
  2. En el encabezado: Accept: application/vnd.ejemplo.v1+json
  3. En el parámetro de consulta: https://api.ejemplo.com/recursos?version=1

Es importante especificar la versión adecuada en tus solicitudes para asegurar la compatibilidad a largo plazo.

// Ejemplo con versión en URL
fetch('https://api.ejemplo.com/v1/usuarios')
  .then(respuesta => respuesta.json())
  .then(datos => console.log(datos));

// Ejemplo con versión en header
fetch('https://api.ejemplo.com/usuarios', {
  headers: {
    'Accept': 'application/vnd.ejemplo.v1+json'
  }
})
  .then(respuesta => respuesta.json())
  .then(datos => console.log(datos));

Ejemplos prácticos con APIs populares

Veamos algunos ejemplos de cómo consumir APIs populares que no requieren autenticación o utilizan métodos simples.

JSONPlaceholder - API para pruebas

JSONPlaceholder es una API gratuita para pruebas y prototipado.

// Obtener todos los posts
fetch('https://jsonplaceholder.typicode.com/posts')
  .then(respuesta => respuesta.json())
  .then(posts => {
    console.log(`Se encontraron ${posts.length} posts`);
    console.log('Primer post:', posts[0]);
  })
  .catch(error => console.error('Error:', error));

// Crear un nuevo post
fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'Nuevo artículo',
    body: 'Contenido del artículo',
    userId: 1
  })
})
  .then(respuesta => respuesta.json())
  .then(nuevoPost => {
    console.log('Post creado:', nuevoPost);
  })
  .catch(error => console.error('Error:', error));

PokeAPI - Información sobre Pokémon

// Obtener información de un Pokémon específico
fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
  .then(respuesta => respuesta.json())
  .then(pokemon => {
    console.log(`Nombre: ${pokemon.name}`);
    console.log(`Altura: ${pokemon.height / 10} metros`);
    console.log(`Peso: ${pokemon.weight / 10} kg`);
    console.log('Tipos:');
    pokemon.types.forEach(tipo => {
      console.log(`- ${tipo.type.name}`);
    });
  })
  .catch(error => console.error('Error:', error));

// Listar los primeros 10 Pokémon
fetch('https://pokeapi.co/api/v2/pokemon?limit=10')
  .then(respuesta => respuesta.json())
  .then(datos => {
    console.log('Primeros 10 Pokémon:');
    datos.results.forEach((pokemon, index) => {
      console.log(`${index + 1}. ${pokemon.name}`);
    });
  })
  .catch(error => console.error('Error:', error));

OpenWeatherMap - Información meteorológica

Esta API requiere una clave API pero tiene un plan gratuito.

// Necesitarás registrarte para obtener una API key: https://openweathermap.org/
const apiKey = 'tu_api_key';
const ciudad = 'Madrid';

// Obtener el clima actual
fetch(`https://api.openweathermap.org/data/2.5/weather?q=${ciudad}&appid=${apiKey}&units=metric&lang=es`)
  .then(respuesta => respuesta.json())
  .then(datos => {
    console.log(`Clima en ${datos.name}:`);
    console.log(`Temperatura: ${datos.main.temp}°C`);
    console.log(`Sensación térmica: ${datos.main.feels_like}°C`);
    console.log(`Humedad: ${datos.main.humidity}%`);
    console.log(`Descripción: ${datos.weather[0].description}`);
  })
  .catch(error => console.error('Error:', error));

Resumen

En este artículo hemos explorado los conceptos fundamentales para consumir APIs externas desde JavaScript. Hemos aprendido sobre los diferentes métodos de autenticación, el manejo de tokens, cómo enfrentar problemas de CORS, optimizar nuestras solicitudes para respetar los límites de tasa, y hemos visto ejemplos prácticos con APIs populares.

El consumo de APIs externas amplía enormemente las posibilidades de nuestras aplicaciones, permitiéndonos integrar datos y funcionalidades de terceros sin tener que implementarlas desde cero. En el próximo artículo, aprenderemos cómo gestionar las respuestas y errores de manera robusta, asegurando que nuestras aplicaciones sean resilientes ante problemas de red o respuestas inesperadas.