Volver al blog
15 de enero de 2025

Entendiendo las Llamadas a APIs: Construyendo una App del Tiempo

Una explicación de desarrollador real de cómo funcionan las llamadas a APIs, desde fetch hasta gestión de estado. Sin BS, solo código práctico.

Entendiendo las Llamadas a APIs: Construyendo una App del Tiempo

Lo Básico (Versión Sin BS)

¿Así que quieres entender cómo funcionan esas llamadas a APIs del tiempo? Déjame explicártelo de la manera que me hubiera gustado a mi cuando empecé.


El Flujo Completo

Paso 1: Usuario Elige una Ciudad

Bastante directo. Usuario hace click en un botón en el modal:

<button onClick={() => {
  setSelectedCity(city); // Guarda la nueva ciudad
  setShowCitySelector(false); // Cierra modal
}}>

Nada espectacular. Solo actualizando estado. Pero aquí es donde se pone interesante...



Paso 2: useEffect Se Activa

Esta es la parte mágica. React tiene esta cosa llamada useEffect que observa variables:

useEffect(() => {
  fetchWeather(selectedCity);
}, [selectedCity]);

¿Ese array al final? [selectedCity] — eso es la lista de dependencias. Es React diciendo literalmente:

"Ey, estoy vigilando selectedCity. Si cambia, ejecutaré fetchWeather de nuevo."

Piénsalo como un perro guardián. Se queda ahí observando una cosa. Cuando esa cosa se mueve, ladra (ejecuta tu función).


Paso 3: Construyendo la Petición de API

Aquí es donde realmente hablamos con la API del tiempo:

const fetchWeather = async (city: City) => {
  // Enciende el spinner de loading
  setLoading(true);
  setError(''); // Limpia errores viejos
   
  try {
    // Construye la URL con todos los datos que necesitamos
    const url = `https://api.open-meteo.com/v1/forecast?` +
                `latitude=${city.lat}&` +
                `longitude=${city.lon}&` +
                `current=temperature_2m,relative_humidity_2m,...&` +
                `daily=temperature_2m_max,temperature_2m_min,...&` +
                `timezone=auto`;
     
    // Hace la petición real
    const response = await fetch(url);
     
    // ¿Funcionó?
    if (!response.ok) {
      throw new Error('API dijo que no');
    }
     
    // Convierte respuesta a objeto JavaScript
    const data = await response.json();
     
    // Guarda en estado
    setWeather({ 
      ...data,
      cityName: city.name,
      country: city.country
    });
     
  } catch (err) {
    console.error('Error:', err);
    setError('No se pudieron obtener datos del tiempo');
  } finally {
    // Siempre apaga loading, error o no
    setLoading(false);
  }
};

Déjame explicar qué está pasando aquí:


La Construcción de la URL

Básicamente estás construyendo un string con todos los parámetros que la API necesita:

  • latitud y longitud - dónde en la Tierra estamos preguntando
  • current - qué datos de tiempo actual queremos (temp, humedad, etc.)
  • daily - qué datos de pronóstico queremos (máx/mín de 7 días)
  • timezone=auto - deja que la API averigüe la zona horaria


La Llamada fetch()

fetch() es la forma nativa de JavaScript para hacer peticiones HTTP. Sé que existe axios y debería aprender a usar cada uno en su lugar pero me he acostumbrado y me cuesta cambiar cuando controlo algo. La llamada es async, lo que significa que toma tiempo. Por eso usamos await — pausa la función hasta que la API responde.

Sin await, tu código seguiría corriendo mientras espera la respuesta, y tentarías de usar datos que aún no existen. Malas noticias.


La Parte response.json()

La API envía texto (específicamente formato JSON). response.json() convierte ese texto en un objeto JavaScript con el que realmente puedes trabajar.

// Lo que la API envía (texto):
'{"temperature": 18.5}'

// Lo que response.json() te da (objeto):
{ temperature: 18.5 }


Manejo de Errores

El bloque try/catch/finally es crucial:

  • try: "Intenta esta cosa arriesgada"
  • catch: "Si algo falla, ejecuta esto en su lugar"
  • finally: "No importa qué pasó, haz esto al final"

Por eso setLoading(false) está en finally — queremos esconder el spinner tanto si tuvimos éxito como si no.


Lo que la API Realmente Retorna

Cuando golpeas ese endpoint, obtienes algo como esto:

{
  "latitude": 40.4168,
  "longitude": -3.7038,
  "current": {
    "time": "2024-12-04T15:00",
    "temperature_2m": 18.5,
    "apparent_temperature": 16.2,
    "relative_humidity_2m": 65,
    "precipitation": 0,
    "weather_code": 2,
    "wind_speed_10m": 12.5,
    "pressure_msl": 1015.3
  },
  "daily": {
    "time": ["2024-12-04", "2024-12-04T15:00", "2024-12-06", "..."],
    "temperature_2m_max": [20, 22, 19, 21, 23, 20, 18],
    "temperature_2m_min": [12, 14, 11, 13, 15, 12, 10],
    "weather_code": [2, 0, 3, 1, 2, 3, 61]
  }
}

¿Ves cómo los datos daily son arrays? Eso son 7 días de pronósticos. Índice 0 es hoy, índice 1 es mañana, etc.


El Flujo Visual

Usuario hace click en "Barcelona"
    ↓
setSelectedCity(barcelona) actualiza estado
    ↓
useEffect detecta el cambio
    ↓
fetchWeather(barcelona) corre
    ↓
setLoading(true) - aparece spinner (el spinner es el tipico dibujito de carga - "estoy trabajando, espera")
    ↓
Construye la URL con coordenadas de Barcelona
    ↓
fetch(url) - envía peticion HTTP, ESPERA respuesta
    ↓
API responde con datos JSON
    ↓
response.json() - convierte a objeto JavaScript
    ↓
setWeather(data) - guarda en estado
    ↓
setLoading(false) - esconde spinner
    ↓
React re-renderiza con nuevos datos
    ↓
Usuario ve el tiempo de Barcelona


Conceptos Clave que Necesitas pillar

Estado de React (useState)

const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');

Estado es cómo React recuerda cosas entre renders. Cuando llamas setWeather(),:

  1. Actualiza el valor
  2. Re-renderiza el componente
  3. Muestra los nuevos datos

No es como una variable normal. Variables regulares se resetean en cada render. Estado persiste.


El Hook useEffect

Este confunde a la gente. Aquí está el trato:

useEffect(() => {
  // Esto corre después de que el componente renderiza
  fetchWeather(selectedCity);
}, [selectedCity]); // Solo re-ejecuta si esto cambia

¿Cuándo corre?

  1. Primer render (siempre)
  2. Cualquier momento que selectedCity cambia

¿Por qué usarlo?

Porque fetchear datos es un "side effect" — algo que afecta el mundo exterior (el servidor de API). React quiere que declares estas cosas explícitamente.


async/await Explicado

// Sin await (incorrecto):
const response = fetch(url);
console.log(response); // Promise { <pending> } - no es lo que quieres

// Con await (correcto):
const response = await fetch(url);
console.log(response); // Objeto Response con datos reales

await literalmente pausa tu función hasta que la promise se resuelve. Pero solo puedes usar await dentro de una función async, por eso escribimos:

const fetchWeather = async (city: City) => {
  // Ahora podemos usar await aquí
}


Por Qué Este Patrón Funciona

Separación de Responsabilidades

  • fetchWeather solo fetchea datos
  • useEffect solo observa cambios
  • Estado solo guarda datos
  • UI solo muestra datos

Cada cosa tiene un trabajo. Fácil de debuggear, fácil de cambiar.


Estado Predecible

Siempre sabes qué está pasando:

  • loading === true → muestra spinner
  • error !== '' → muestra error
  • weather !== null → muestra tiempo

No hay estados extraños intermedios.


Actualizaciones Automáticas

¿Cambia la ciudad? React maneja el resto:

  1. useEffect trigger
  2. Datos se fetchean
  3. Estado se actualiza
  4. UI re-renderiza

No actualizas el DOM manualmente en ningún lado. React lo hace.


Un Ejemplo Más Simple

Si todo eso fue demasiado, aquí está la versión absolutely minimal:

// 1. Usuario elige una ciudad
const city = { lat: 40.4168, lon: -3.7038, name: 'Madrid' };

// 2. Construye URL
const url = `https://api.open-meteo.com/v1/forecast?latitude=40.4168&longitude=-3.7038&current=temperature_2m`;

// 3. Fetch datos
const response = await fetch(url);
const data = await response.json();

// 4. Muéstralo
console.log(data.current.temperature_2m); // 18.5

// 5. Renderiza
return <div>{data.current.temperature_2m}°</div>

Eso es todo. Todo lo demás es solo manejar errores, estados de loading, y hacerlo production-ready.


Errores Comunes que Cometí

1. Olvidar async/await

// Incorrecto - fetch retorna una promise, no datos
const data = fetch(url).json();

// Correcto - espera cada paso
const response = await fetch(url);
const data = await response.json();


2. No checkear response.ok

// Incorrecto - asume que siempre funciona
const data = await response.json();

// Correcto - checkea errores
if (!response.ok) throw new Error('Failed');
const data = await response.json();


3. Poner fetch en el cuerpo del componente

// Incorrecto - corre en cada render, loop infinito
function Component() {
  fetch(url); // NO HAGAS ESTO
  return <div>...</div>
}

// Correcto - dentro de useEffect
function Component() {
  useEffect(() => {
    fetch(url);
  }, []);
}

Por Qué las APIs Usan Coordenadas en Vez de Nombres de Ciudades

¿Te preguntarás: ¿por qué no enviar solo el nombre de la ciudad?

Precisión: "Paris" podría ser Paris, Francia o Paris, Texas. Las coordenadas son exactas.

Sin Traducción: Funciona en cualquier idioma sin conversión.

Flexibilidad: Puedes obtener el tiempo de CUALQUIER punto en la Tierra, no solo ciudades nombradas.

Velocidad: La API no necesita buscar nombres de ciudades en una base de datos primero.

Tu frontend mantiene los nombres de ciudades para mostrar. La API solo quiere lat/lon.

La API esta muy bien hecha, está claro, no la he hecho yo . Además es gratis :D lmao pwnz rofl


Pensamientos Finales

Las llamadas a APIs parecen complejas al principio, pero siempre es el mismo patrón:

1 - Construir URL con parámetros
2 - Enviar peticion (fetch)
3 - Esperar respuesta (await)
4 - Parsear respuesta (json)
5 - Actualizar estado
6 - React re-renderiza

Una vez que pillas este patrón, puedes llamar cualquier API. Tiempo, GitHub, Stripe, lo que sea. La única diferencia es qué parámetros envías y qué datos vuelven.

La clave es entender que todo es asíncrono. Envías una peticion, esperas una respuesta, entonces manejas esa respuesta. Como pedir comida — no te quedas parado en el mostrador esperando, te sientas y haces otras cosas hasta que llegue.

Sigue construyendo, sigue rompiendo cosas, y eventualmente esto se vuelve segunda naturaleza.


PD: Esta app se hizo en solo un page.tsx, los siguientes pasos son como siempre;

Rendimiento (server components), Arquitectura (dividir en componentes), UI/UX (cosas visuales), Código (funciones de cache), Nuevas features (tu mente, tus ideas), Escalabilidad (menos llamadas al servidor, testing)

Quizás trabaje en ello si me aburro mucho pero tengo una hija :)


Actualización de Producción

08/01/26

Vale, así que esta app del tiempo evolucionó mucho más allá del ejemplo básico de useEffect de arriba. Esto es lo que se convirtió.


Lo que Realmente Envié

Infraestructura PWA (El verdadero negocio)

La convertí en una PWA de producción completa con Serwist (fork moderno de Workbox):

  • Service Worker en app/sw.ts con estrategias de caching inteligentes:

    • API del Tiempo: StaleWhileRevalidate con cache de 30min → respuestas instantáneas, actualizaciones en background
    • Páginas HTML: NetworkFirst → siempre intenta fresco, fallback a cache cuando offline
    • Imágenes/Icons: CacheFirst con TTL de 30 días, máx 100 entradas
    • CSS/JS: CacheFirst con cache de 1 año para assets estáticos con hash
    • Google Fonts: CacheFirst con cache de 1 año
  • Sistema de auto-update que checkea cada hora nuevas versiones

    • Notificación toast personalizada (abajo-derecha) con botón "Actualizar"
    • Cambio de versión suave sin reload completo
    • No más código obsoleto sentado en el cache del usuario
  • Prompts de instalación para cada plataforma:

    • Android/Desktop: beforeinstallprompt nativo con botón flotante gradiente
    • iOS: Modal paso a paso con instrucciones de "Add to Home Screen"
    • Corre en modo standalone (pantalla completa, sin chrome del navegador)

Evolución de la Capa de Datos

Antes: useEffect básico + fetch + gestión manual de estado (ver arriba)

Ahora: Arquitectura de datos de producción

// SWR reemplaza useEffect completamente
const { data: weatherData, error, isLoading } = useSWR(
  selectedCity ? `weather-${selectedCity.name}` : null,
  () => fetchWeatherData(selectedCity!),
  {
    revalidateOnFocus: false,
    revalidateOnReconnect: true,
    refreshInterval: 3600000, // 1 hora
    fallbackData: cachedWeather,
  }
);

Por qué SWR sobre useEffect:

  • Deduplica peticiones (no double-fetching en React 18+ strict mode)
  • Cache built-in con revalidación configurable
  • Retry automático con exponential backoff
  • Revalidación en focus deshabilitada (no malgastes API calls)
  • Revalidación en reconnect habilitada (datos frescos cuando vuelves online)

Schemas de Zod para validación en runtime:

const WeatherResponseSchema = z.object({
  current: z.object({
    temperature_2m: z.number(),
    apparent_temperature: z.number(),
    // ... 10+ campos con null coalescing
  }),
  daily: z.object({
    time: z.array(z.string()),
    temperature_2m_max: z.array(z.number()),
    // ... maneja null/undefined de la API
  })
});

¿La API envía nulls? Zod lo atrapa. ¿Campo faltante? Zod proporciona defaults. TypeScript feliz, UI nunca crashea.


Sistema de Geolocalización

Auto-detección en primer load:

// Geolocalización del navegador con timeout + caching
navigator.geolocation.getCurrentPosition(
  async (position) => {
    const { latitude, longitude } = position.coords;

    // Reverse geocoding vía Nominatim
    const response = await fetch(
      `https://nominatim.openstreetmap.org/reverse?` +
      `lat=${latitude}&lon=${longitude}&format=json&accept-language=es`
    );

    const data = await response.json();
    setSelectedCity({
      name: data.address.city || data.address.town,
      lat: latitude,
      lon: longitude,
      country: data.address.country
    });
  },
  (error) => {
    // Fallback a Madrid de lista curada
    setSelectedCity(popularCities[0]);
  },
  { timeout: 10000, maximumAge: 300000 } // 10s timeout, 5min cache
);

Mensajes de error conscientes del contexto:

  • Modo PWA: "Allow location in app settings"
  • Navegador: "Allow location in browser settings"
  • Desktop: Mensaje fallback genérico

28 ciudades pre-curadas a través de 6 continentes como fallback, sin abuso de API de geocoding.


Mejoras de Arquitectura

División de componentes (adiós single-file madness):

components/weather/sections/
├── HeaderBar.tsx          # Toggle de tema, unidades de temp
├── CitySelector.tsx       # Búsqueda + filtros de continente
├── CityModal.tsx          # Selector de ciudad con 28 curadas + búsqueda
├── CurrentWeather.tsx     # Sección hero con condiciones actuales
├── WeatherDetails.tsx     # Humedad, viento, precip, presión, UV
├── HourlyForecast.tsx     # Carrusel 24h con temps + precipitación
├── WeatherAlerts.tsx      # Banners de alerta de lluvia/calor (sorry si límites !== tus sensaciones térmicas :P)
├── Forecast.tsx           # Pronóstico de 7/16 días en cards
└── FooterInfo.tsx         # Attribution + badge de datos vivos

Capa de servicios:

app/weather/services/
├── weather-service.ts     # Fetcher SWR + validación Zod
├── city-utils.ts          # Geolocalización + ciudades populares + mapeo continente
└── weather-utils.tsx      # Mapeo código WMO → icon/descripción (28+ condiciones)

Códigos de Tiempo WMO interpretados:

  • 0-3: Despejado a nublado (5 variaciones)
  • 45, 48: Variantes de niebla
  • 51-67: Llovizna a lluvia (10 variaciones)
  • 71-77: Nieve (4 variaciones)
  • 80-99: Chubascos + tormentas eléctricas (9 variaciones)

Cada código mapea a ícono Lucide + descripción en español. UI siempre muestra contexto, no solo números.


Lo que el Stack se Convirtió

Antes: Next.js + React + fetch básico

Ahora:

  • Next.js 16 (App Router, React 19)
  • SWR 2.3.7 (data fetching inteligente)
  • Serwist 9.5.0 (gestión de service worker)
  • Zod 3.25.76 (validación en runtime)
  • Lucide React 0.469.0 (40+ iconos de tiempo)
  • Vercel Analytics + Speed Insights (monitoreo Core Web Vitals)
  • Tailwind CSS 3.4.18 (tokens semánticos para theming)

Features que Importan

Offline-first:

  • Funciona con cero red después de primera visita
  • Datos del tiempo cacheados 30 minutos
  • Todos los assets (CSS, JS, imágenes) cacheados indefinidamente
  • Service worker sirve desde cache, actualiza en background

Rendimiento:

  • Render inicial server-side (HTML con fallback data)
  • Cliente hidrata con SWR (no double-fetch)
  • Carga perezosa de imágenes vía SVGs de Lucide (sin imágenes raster)
  • Revalidación hourly (configurable en SWR)

UX polish:

  • Alertas de lluvia cuando precipitación > umbral
  • Alertas de calor cuando temp excede límite
  • Carrusel de pronóstico hourly (24h con temps + % precip)
  • Toggle 7/16 días
  • Toggle °C/°F
  • Filtro de continente para búsqueda de ciudades
  • Cambio de tema (light/dark) sincronizado a preferencias del sistema
  • Badge de datos vivos mostrando hora de última actualización

Cross-platform:

  • Instalable en iOS, Android, desktop
  • Modo standalone (sin UI de navegador)
  • Manifest PWA correcto con icons (192x192, 512x512, Apple touch)
  • Localización española completa

Lo que Añadiría Siguiente

Gráficos para tendencias hourly:

  • Curva de temperatura (próximas 24h)
  • Gráfico de probabilidad de precipitación
  • Visualización de velocidad del viento
  • Carga perezosa (no hinchar initial bundle)

Historial de ubicación:

  • Guardar últimas 5 ciudades buscadas en localStorage
  • Dropdown de cambio rápido
  • Sincronizar entre dispositivos si estás logueado

Notificaciones del tiempo:

  • Push API para alertas de clima severo
  • Umbrales personalizables (lluvia > X%, temp > Y°)
  • Sync en background para checkear mientras app cerrada

Métricas de rendimiento:

  • Exponer datos de Vercel Speed Insights a usuarios
  • Mostrar cache hit rate
  • Mostrar tiempos de respuesta de API

El Camino de Evolución

v1: Single page.tsx con useEffect + fetch (este post del blog)
    ↓
v2: Componentes divididos + SWR + validación Zod
    ↓
v3: Añadir PWA (service worker + manifest + prompts de instalación)
    ↓
v4: Geolocalización + reverse geocoding + ciudades curadas
    ↓
v5: Múltiples estrategias de cache + auto-updates
    ↓
Current: PWA de producción con monitoreo
    ↓
Future: Gráficos + historial + push notifications

Empezó como proyecto de aprendizaje para entender llamadas a APIs. Terminó construyendo infraestructura de producción porque why not.

¿El ejemplo de useEffect de arriba? Todavía válido para aprender. Pero apps de producción necesitan SWR, service workers, manejo de errores adecuado, estrategias de caching, y soporte offline.

Esto es lo que "production-ready" realmente significa. No solo "funciona", sino "funciona en todas partes, siempre, incluso offline, y se actualiza automáticamente."


Live: https://weather.stackbp.es Stack: Next.js 16, React 19, TypeScript, SWR, Serwist, Zod Status: Enviado, monitoreando Core Web Vitals en Vercel

Volver al blog
Entendiendo las Llamadas a APIs: Construyendo una App del Tiempo | bpstack