← All projects

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

embed
from 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']}"
                "&current=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';
});