Python · Vercel Functions · Open-Meteo API
Weather Dashboard
Search any city and get current conditions plus a 5-day forecast, proxied server-side through a free weather API.
The backend geocodes the city name and fetches the forecast from the free Open-Meteo API (no key required), then returns one clean JSON payload. Proxying through our own API avoids exposing a third-party response shape to the client and sidesteps CORS entirely. Written in Python using only the standard library.
Live demo
embedfrom http.server import BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs, quote
from urllib.request import urlopen
from urllib.error import URLError
import json
class handler(BaseHTTPRequestHandler):
def do_GET(self):
query = parse_qs(urlparse(self.path).query)
city = (query.get('city', [''])[0] or '').strip()
if not city:
self._json(400, {'error': 'city is required'})
return
try:
geo_url = f"https://geocoding-api.open-meteo.com/v1/search?name={quote(city)}&count=1"
with urlopen(geo_url, timeout=8) as r:
geo = json.loads(r.read())
results = geo.get('results') or []
if not results:
self._json(404, {'error': f'No location found for "{city}"'})
return
place = results[0]
forecast_url = (
"https://api.open-meteo.com/v1/forecast"
f"?latitude={place['latitude']}&longitude={place['longitude']}"
"¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code"
"&daily=temperature_2m_max,temperature_2m_min,weather_code"
"&timezone=auto"
)
with urlopen(forecast_url, timeout=8) as r:
forecast = json.loads(r.read())
self._json(200, {
'place': {
'name': place.get('name'),
'country': place.get('country'),
'admin1': place.get('admin1'),
},
'current': forecast.get('current'),
'daily': forecast.get('daily'),
})
except URLError as e:
self._json(502, {'error': 'Upstream weather service failed', 'detail': str(e)})
except Exception as e:
self._json(502, {'error': 'Upstream weather service failed', 'detail': str(e)})
def _json(self, status, payload):
body = json.dumps(payload).encode('utf-8')
self.send_response(status)
self.send_header('Content-type', 'application/json')
self.send_header('Content-length', str(len(body)))
self.end_headers()
self.wfile.write(body)
const form = document.getElementById('form');
const cityInput = document.getElementById('city');
const errorBox = document.getElementById('error');
const result = document.getElementById('result');
const WMO = {
0: 'Clear sky', 1: 'Mostly clear', 2: 'Partly cloudy', 3: 'Overcast',
45: 'Fog', 48: 'Fog', 51: 'Light drizzle', 61: 'Light rain', 63: 'Rain',
65: 'Heavy rain', 71: 'Light snow', 73: 'Snow', 75: 'Heavy snow',
80: 'Rain showers', 95: 'Thunderstorm',
};
function describe(code) {
return WMO[code] || 'Mixed conditions';
}
function dayLabel(iso) {
return new Date(iso).toLocaleDateString(undefined, { weekday: 'short' });
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
errorBox.textContent = '';
result.style.display = 'none';
const res = await fetch(`/api/weather?city=${encodeURIComponent(cityInput.value.trim())}`);
const data = await res.json();
if (!res.ok) {
errorBox.textContent = data.error || 'Could not fetch weather.';
return;
}
document.getElementById('place').textContent =
[data.place.name, data.place.admin1, data.place.country].filter(Boolean).join(', ');
document.getElementById('temp').textContent = `${Math.round(data.current.temperature_2m)}°C`;
document.getElementById('meta').innerHTML =
`${describe(data.current.weather_code)}<br>Humidity ${data.current.relative_humidity_2m}% · Wind ${Math.round(data.current.wind_speed_10m)} km/h`;
const days = document.getElementById('days');
days.innerHTML = '';
const highs = data.daily.temperature_2m_max.slice(0, 5);
const lows = data.daily.temperature_2m_min.slice(0, 5);
const weekMax = Math.max(...highs);
const weekMin = Math.min(...lows);
const span = Math.max(1, weekMax - weekMin);
data.daily.time.slice(0, 5).forEach((date, i) => {
const hi = Math.round(highs[i]);
const lo = Math.round(lows[i]);
const top = ((weekMax - highs[i]) / span) * 100;
const bottom = ((lows[i] - weekMin) / span) * 100;
const el = document.createElement('div');
el.className = 'day';
el.innerHTML = `
<div class="hi">${hi}°</div>
<div class="track">
<div class="bar" style="top:${top}%;bottom:${bottom}%"></div>
</div>
<div class="lo">${lo}°</div>
<div class="d">${dayLabel(date)}</div>
`;
days.appendChild(el);
});
result.style.display = 'block';
});