Case Study: How Weather Apps Work — From API to Your Phone Screen
The Scenario
You open your phone's weather app. Within a second, you see the current temperature, a five-day forecast, a radar map, humidity, wind speed, UV index, and a sunrise/sunset time. It feels seamless — almost magical.
But behind that simple interface is a cascade of API calls, data transformations, and rendering decisions that happen in the blink of an eye. Let's trace the entire journey of a weather forecast, from the moment you tap the app icon to the moment numbers appear on your screen.
Step 1: Where Does the Weather Data Come From?
Weather data originates from a global network of physical sensors:
- Surface weather stations (~10,000 worldwide) measure temperature, humidity, wind, and pressure
- Weather balloons (launched twice daily from ~900 sites) measure conditions through the atmosphere
- Satellites (geostationary and polar-orbiting) capture cloud cover, surface temperature, and atmospheric data
- Radar installations (~160 in the US alone) track precipitation
- Ocean buoys measure sea surface temperature and wave height
- Aircraft sensors on commercial flights report conditions at cruising altitude
This raw data flows to national meteorological agencies (like the US National Weather Service, the UK Met Office, or the Japan Meteorological Agency), which run massive computer simulations — numerical weather prediction models — to produce forecasts.
The agencies then publish the processed data through APIs. In the US, the NWS offers a free public API at api.weather.gov.
Step 2: The API Request
When you open your weather app, your phone sends an API request. Here's what that looks like in Python:
import requests
# Step 2a: Get the forecast office and grid point for a location
# The NWS API requires a two-step lookup
lat, lon = 45.5152, -122.6784 # Portland, OR
metadata_url = f"https://api.weather.gov/points/{lat},{lon}"
headers = {"User-Agent": "(WeatherApp, contact@example.com)"}
metadata = requests.get(metadata_url, headers=headers, timeout=10)
metadata_data = metadata.json()
# The metadata response tells us which forecast endpoint to use
forecast_url = metadata_data["properties"]["forecast"]
print(f"Forecast endpoint: {forecast_url}")
# Step 2b: Get the actual forecast
forecast = requests.get(forecast_url, headers=headers, timeout=10)
forecast_data = forecast.json()
# Extract periods (each period is a forecast window)
periods = forecast_data["properties"]["periods"]
for period in periods[:4]:
print(f" {period['name']:<20} {period['temperature']}°{period['temperatureUnit']}"
f" {period['shortForecast']}")
# Output (varies by actual weather):
# Forecast endpoint: https://api.weather.gov/gridpoints/PQR/112,103/forecast
# Today 58°F Partly Cloudy
# Tonight 45°F Mostly Clear
# Wednesday 62°F Sunny
# Wednesday Night 47°F Partly Cloudy
Notice the two-step process: first you convert coordinates to a grid point, then you request the forecast for that grid point. Many APIs require this kind of multi-step lookup.
Step 3: The API Response Structure
The NWS forecast response is a deeply nested JSON document. Here's a simplified version of what comes back:
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-122.73, 45.57], [-122.71, 45.54], ...]]
},
"properties": {
"updated": "2025-03-12T14:30:00+00:00",
"units": "us",
"forecastGenerator": "BaselineForecastGenerator",
"periods": [
{
"number": 1,
"name": "Today",
"startTime": "2025-03-12T06:00:00-07:00",
"endTime": "2025-03-12T18:00:00-07:00",
"isDaytime": true,
"temperature": 58,
"temperatureUnit": "F",
"windSpeed": "5 to 10 mph",
"windDirection": "NW",
"icon": "https://api.weather.gov/icons/land/day/sct",
"shortForecast": "Partly Cloudy",
"detailedForecast": "Partly cloudy, with a high near 58. Northwest wind 5 to 10 mph."
},
{
"number": 2,
"name": "Tonight",
"temperature": 45,
"isDaytime": false,
"shortForecast": "Mostly Clear"
}
]
}
}
To navigate this structure, the app drills through layers of nesting:
# Navigate to the first forecast period's temperature
temp = forecast_data["properties"]["periods"][0]["temperature"]
# Get all period names and temperatures
for period in forecast_data["properties"]["periods"]:
name = period["name"]
temp = period["temperature"]
unit = period["temperatureUnit"]
forecast_text = period["shortForecast"]
Step 4: Data Transformation
The raw API data isn't what you see on screen. The app transforms it:
def transform_forecast(raw_periods: list[dict]) -> list[dict]:
"""Transform raw NWS periods into app-friendly format."""
transformed = []
for period in raw_periods:
entry = {
"name": period["name"],
"temp": period["temperature"],
"unit": period["temperatureUnit"],
"forecast": period["shortForecast"],
"is_day": period.get("isDaytime", True),
"wind": period.get("windSpeed", "Calm"),
# Convert to Celsius for international users
"temp_c": round((period["temperature"] - 32) * 5 / 9, 1)
if period["temperatureUnit"] == "F" else period["temperature"],
}
# Map forecast text to emoji for the UI
condition = period["shortForecast"].lower()
if "sunny" in condition or "clear" in condition:
entry["icon"] = "sun"
elif "cloud" in condition:
entry["icon"] = "cloud"
elif "rain" in condition or "shower" in condition:
entry["icon"] = "rain"
elif "snow" in condition:
entry["icon"] = "snow"
elif "thunder" in condition:
entry["icon"] = "storm"
else:
entry["icon"] = "default"
transformed.append(entry)
return transformed
Step 5: Multiple API Calls in Parallel
A full-featured weather app doesn't make just one API call. It makes several:
| Data | Source API | Why |
|---|---|---|
| Current conditions | Weather API (current endpoint) | Temperature, humidity, wind right now |
| 7-day forecast | Weather API (forecast endpoint) | Planning for the week |
| Hourly forecast | Weather API (hourly endpoint) | "Will it rain at 3pm?" |
| Radar imagery | Radar API | Visual precipitation map |
| Air quality | AirNow API or similar | Health advisories |
| UV index | UV API | Sunburn risk |
| Sunrise/sunset | Astronomy API | Display times |
| Severe alerts | Weather alerts API | Tornado warnings, etc. |
Each of these is a separate HTTP request to a separate endpoint (sometimes a separate API entirely). The app fires them off in parallel (using techniques beyond CS1), combines the results, and renders the unified display.
import requests
def fetch_all_weather_data(lat: float, lon: float) -> dict:
"""Fetch weather data from multiple endpoints (sequential version)."""
base_headers = {"User-Agent": "(WeatherApp, contact@example.com)"}
result = {}
# Step 1: Get grid point metadata
meta_url = f"https://api.weather.gov/points/{lat},{lon}"
meta_resp = requests.get(meta_url, headers=base_headers, timeout=10)
if not meta_resp.ok:
return {"error": f"Metadata request failed: {meta_resp.status_code}"}
meta = meta_resp.json()["properties"]
result["station"] = meta.get("radarStation", "Unknown")
# Step 2: Get forecast
forecast_url = meta.get("forecast", "")
if forecast_url:
fc_resp = requests.get(forecast_url, headers=base_headers, timeout=10)
if fc_resp.ok:
periods = fc_resp.json()["properties"]["periods"]
result["forecast"] = [
{"name": p["name"], "temp": p["temperature"],
"condition": p["shortForecast"]}
for p in periods[:6]
]
# Step 3: Get hourly forecast
hourly_url = meta.get("forecastHourly", "")
if hourly_url:
hr_resp = requests.get(hourly_url, headers=base_headers, timeout=10)
if hr_resp.ok:
hours = hr_resp.json()["properties"]["periods"]
result["hourly"] = [
{"time": h["startTime"], "temp": h["temperature"]}
for h in hours[:12]
]
# Step 4: Get alerts for the zone
zone = meta.get("forecastZone", "").split("/")[-1]
if zone:
alert_url = f"https://api.weather.gov/alerts/active/zone/{zone}"
alert_resp = requests.get(alert_url, headers=base_headers, timeout=10)
if alert_resp.ok:
alerts = alert_resp.json().get("features", [])
result["alerts"] = [
a["properties"]["headline"] for a in alerts
]
return result
Step 6: Caching and Efficiency
Weather apps don't make fresh API calls every time you glance at your phone. They use caching:
import json
import time
from pathlib import Path
CACHE_FILE = Path("weather_cache.json")
CACHE_MAX_AGE = 600 # 10 minutes in seconds
def get_cached_forecast(lat: float, lon: float) -> dict | None:
"""Return cached forecast if it's fresh enough."""
if not CACHE_FILE.exists():
return None
with open(CACHE_FILE, "r") as f:
cache = json.load(f)
# Check if the cache is for the right location and is fresh
if (cache.get("lat") == lat and cache.get("lon") == lon
and time.time() - cache.get("timestamp", 0) < CACHE_MAX_AGE):
print(" Using cached forecast")
return cache.get("data")
return None # Cache miss — need to fetch fresh data
def save_to_cache(lat: float, lon: float, data: dict) -> None:
"""Save forecast data to the cache."""
cache = {
"lat": lat,
"lon": lon,
"timestamp": time.time(),
"data": data,
}
with open(CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
This is the same JSON persistence pattern from Chapter 10 — applied to a new problem.
The Complete Picture
┌───────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────┐
│ SENSORS │ → │ NWS/NOAA │ → │ PUBLIC API │ → │ YOUR │
│ │ │ Models run │ │ JSON served │ │ PYTHON │
│ Stations │ │ forecasts │ │ on request │ │ SCRIPT │
│ Balloons │ │ computed │ │ │ │ │
│ Satellites│ │ │ │ api.weather │ │ requests │
│ Radar │ │ │ │ .gov/... │ │ .get() │
└───────────┘ └──────────────┘ └─────────────┘ └──────────┘
│
▼
┌──────────┐
│ DISPLAY │
│ │
│ Terminal │
│ or GUI │
└──────────┘
Every weather app — from Apple Weather to Weather Underground to your own Python script — follows this same architecture. The only differences are scale, polish, and how many API sources are combined.
Discussion Questions
-
The NWS API requires a
User-Agentheader identifying your application. Why do you think they require this? What would happen if they didn't? -
The weather app makes 4-8 API calls to display one screen. If each call takes 200 milliseconds, that's 1.6 seconds. How might a professional app make this faster? (Hint: think about what "parallel" means.)
-
The NWS API is free and publicly funded. Many commercial weather APIs charge per request. What trade-offs would a developer face when choosing between a free government API and a paid commercial API?
-
If you were building a weather app for farmers, what additional data sources might you need beyond standard weather APIs? How would you combine them into a useful pipeline?
-
The cache implementation above stores data for 10 minutes. For a weather app, is 10 minutes a reasonable cache duration? What factors would affect this decision?
Mini-Project
Build a simple command-line weather tool in Python that:
1. Accepts a city name or zip code from the user
2. Makes a request to the NWS API (at api.weather.gov) — or uses the JSONPlaceholder API as a stand-in if you don't have reliable internet
3. Displays the current conditions and a 3-day forecast
4. Caches the result to a JSON file
5. Uses the cached result if it's less than 10 minutes old
Handle all error cases: no internet, invalid location, API errors, and corrupted cache files.
References
- National Weather Service API documentation: https://www.weather.gov/documentation/services-web-api (Tier 1 — official government documentation)
- World Meteorological Organization (WMO) overview of the Global Observing System: https://public.wmo.int/ (Tier 1)
- The NWS processes approximately 76 billion observations per day from its sensor network. (Tier 2 — based on NOAA published statistics)