skip to content
kursataknc
back to docs

$ cat weather-pipeline.md

Weather Dashboard — API Proxy, Fan-Out, and Caching Strategy

Published: April 2026 · 4 min read

Weather data looks like a solved problem until you actually build it. You need geocoding, current conditions, an hourly forecast, and air quality — each from a different endpoint, each with a different freshness requirement, each returning a response shape that will occasionally be malformed. The wrong architecture makes all four of those a single point of failure.

The proxy is a privacy decision, not a performance one. Every request to Open-Meteo goes through /api/weather on this server. The visitor's IP never reaches the upstream provider. This matters because geolocation APIs log every request IP they receive — a user searching "Paris" from their home IP is, from Open-Meteo's perspective, a data point. Proxying eliminates that exposure entirely. The latency cost is real but small; the privacy benefit is permanent.

Parallel fan-out with Promise.all. A naïve implementation would await geocoding, then await forecast, then await air quality — three round trips in series. Instead, once a city is selected, the route handler runs all three upstream calls in a single Promise.all: forecast (current + hourly + daily in one Open-Meteo request), air quality (PM2.5, PM10, ozone, NO₂, European AQI), and a metadata wrapper. Total latency is bounded by the slowest of the three, not their sum. On a warm cache this doesn't matter; on a cold request it is the difference between a 900ms and a 300ms response.

Three cache TTLs, each justified. Geocoding results (city name → coordinates) are cached for 24 hours — cities do not move, and a longer TTL would be equally valid. Forecast data revalidates every 15 minutes because Open-Meteo updates its models on roughly that cadence; caching longer would serve stale conditions, caching shorter would waste upstream quota. Air quality uses the same 15-minute TTL since it comes from the same provider on the same update schedule. The Next.js data cache keys by full URL, so two users requesting the same city within a 15-minute window share one upstream call, not two.

Schema validation as a contract. Open-Meteo is a reliable provider, but no external API is unconditionally trustworthy. Every response passes through a Zod schema before the route handler returns anything. If the response is missing a required field or contains an unexpected type — which happens during Open-Meteo maintenance windows — the handler returns a 503 with a generic message. This is a deliberate choice: a 503 tells the client to retry later, a 500 implies a bug on this side, and an unvalidated bad response that passes through to the renderer produces a runtime crash that is harder to diagnose than a clean HTTP error.

Domain abstraction over raw API output. Open-Meteo returns numeric WMO weather codes: 0 is clear sky, 45 is fog, 95 is a thunderstorm. There are over 100 codes. Rather than scattering if code === 95 checks across components, a lookup table in lib/weather/wmo.ts maps every code to a label and an icon name. Unknown codes return "Unknown conditions" rather than crashing. This single file is the boundary between the upstream API's vocabulary and the application's vocabulary — changing providers means updating one file, not hunting through twelve components.

State that lives on the client. Favorites, the last-viewed city, and the unit preference (metric / imperial) are stored in a Zustand store persisted to localStorage. Switching units is a re-render, not a fetch — Open-Meteo returns both Celsius and Fahrenheit in the same response, so there is no reason to go back to the network. The store restores the last-viewed city on mount and re-fetches current conditions only if the data is stale. This keeps the perceived performance high without any cache invalidation complexity on the server.

Forecast layout. HourlyForecast is a horizontal scroll strip with snap scrolling. DailyForecast is a 7-row stacked list with min/max temperatures and precipitation totals. Both render from the same forecast payload — Open-Meteo returns hourly and daily in one response, so there is no extra latency for the second view.