weather terminal :: free public weather api :: no key, no signup, JSON + CORS
  Base URL      http://localhost:3000
  Auth          none. No API key, no signup, no quota registration.
  Format        JSON only (application/json; charset=utf-8)
  CORS          Access-Control-Allow-Origin: *   (browser calls work directly)
  Methods       GET, HEAD, OPTIONS
  Upstream      open-meteo.com  (free, non-commercial-friendly, CC BY 4.0)
  Caching       weather responses cached 10 min, geocoding cached 24 h
  Rate limit    60 requests / 60 s per IP  (headers: X-RateLimit-*)

  Every endpoint answers with a top level "ok" boolean. On failure the body is
  { "ok": false, "error": { "code": "...", "message": "..." } } and the HTTP status
  is 400 / 404 / 429 / 5xx.
  GET /api/weather      current conditions + daily forecast + hourly series
                        ?city=Warsaw
                        ?lat=52.2297&lon=21.0122

  GET /api/forecast     alias of /api/weather (identical payload)

  GET /api/current      current conditions only (no daily / hourly arrays)
                        ?city=Tokyo

  GET /api/geocode      resolve a place name to coordinates
                        ?q=Lisbon&count=5

  GET /api/health       service status, cache statistics, counters

  GET /api              machine readable endpoint index
  NAME       TYPE      DEFAULT    APPLIES TO           DESCRIPTION
  --------------------------------------------------------------------------------------------
  city       string    -          weather,forecast,    City name. Resolved through the geocoder;
                                  current             the best match is used. Alias: q, name.
  lat        number    -          weather,forecast,    Latitude  -90 .. 90.   Alias: latitude.
                                  current
  lon        number    -          weather,forecast,    Longitude -180 .. 180. Alias: lng, longitude.
                                  current
  days       integer   7          weather,forecast     Forecast days, 1 .. 16.
  hours      integer   24         weather,forecast     Hourly entries returned, 0 .. 48.
  units      string    metric     all weather calls    "metric" (C, km/h, mm) or
                                                       "imperial" (F, mph, inch).
  timezone   string    auto       all weather calls    IANA name, e.g. Europe/Warsaw, or "auto"
                                                       (resolved from the coordinates). Alias: tz.
  lang       string    en         city lookup          Geocoder language, e.g. en, de, ru, pl.
  q          string    -          geocode              Place name to search (required).
  count      integer   8          geocode              Results returned, 1 .. 20.

  Either city OR the pair lat + lon is required for weather calls.
  If both are given, city wins.
{
  "ok": true,
  "location": {
    "name": "Warsaw", "country": "Poland", "country_code": "PL", "admin1": "Mazovia",
    "latitude": 52.23, "longitude": 21.01, "elevation": 113.0, "population": 1702139,
    "timezone": "Europe/Warsaw", "timezone_abbreviation": "GMT+2", "utc_offset_seconds": 7200
  },
  "units": { "temperature": "C", "wind_speed": "km/h", "pressure": "hPa",
             "precipitation": "mm", "humidity": "%" },
  "current": {
    "time": "2026-08-22T19:30", "is_day": 1,
    "weather_code": 61, "condition": "RAIN", "description": "Slight rain",
    "temperature": 18.4, "apparent_temperature": 17.9, "humidity": 71,
    "pressure": 1013.2, "surface_pressure": 1000.8, "cloud_cover": 84,
    "precipitation": 0.4, "rain": 0.4, "showers": 0.0, "snowfall": 0.0,
    "wind_speed": 12.4, "wind_gusts": 28.1, "wind_direction": 215,
    "wind_direction_cardinal": "SW",
    "sunrise": "2026-08-22T05:42", "sunset": "2026-08-22T19:58", "uv_index_max": 5.1
  },
  "daily": [
    { "date": "2026-08-22", "weather_code": 61, "condition": "RAIN",
      "description": "Slight rain", "temp_max": 21.3, "temp_min": 13.1,
      "feels_max": 20.8, "feels_min": 12.4,
      "sunrise": "2026-08-22T05:42", "sunset": "2026-08-22T19:58",
      "uv_index_max": 5.1, "precipitation_sum": 3.2, "precipitation_probability": 70,
      "wind_speed_max": 18.0, "wind_direction": 220, "wind_direction_cardinal": "SW" }
  ],
  "hourly": [
    { "time": "2026-08-22T20:00", "temperature": 17.8, "weather_code": 61,
      "condition": "RAIN", "is_day": 0, "precipitation_probability": 62 }
  ],
  "meta": {
    "source": "open-meteo.com",
    "attribution": "Weather data by Open-Meteo.com (CC BY 4.0)",
    "cached": false, "cache_age_seconds": 0,
    "generated_at": "2026-08-22T17:30:11.402Z"
  }
}
  condition is a stable, human friendly string derived from the WMO
  weather_code. Use it to pick an icon, a colour or an ASCII scene:

  SUNNY          code 0, daytime           CLEAR          code 0, night
  PARTLY         1, 2                      CLOUDY         (reserved / generic)
  OVERCAST       3                         FOG            45, 48
  DRIZZLE        51, 53, 55                RAIN           61, 63, 65, 80, 81, 82
  FREEZING_RAIN  56, 57, 66, 67            SNOW           71, 73, 75, 77, 85, 86
  STORM          95, 96, 99                HAIL           (reserved, see STORM codes 96/99)

  description carries the exact WMO wording, e.g. "Violent rain showers".
  # current weather + 7 day forecast by city
  curl -s "$BASE/api/weather?city=Warsaw" | jq '.current'

  # by coordinates, imperial units, 3 days
  curl -s "$BASE/api/weather?lat=52.2297&lon=21.0122&days=3&units=imperial"

  # just the current conditions
  curl -s "$BASE/api/current?city=Tokyo" | jq '.current.temperature'

  # geocoding
  curl -s "$BASE/api/geocode?q=Lisbon&count=3" | jq '.results[].name'

  # inspect rate limit headers
  curl -sD - -o /dev/null "$BASE/api/weather?city=Oslo" | grep -i ratelimit
  const BASE = "http://localhost:3000";

  async function getWeather(city) {
    const res = await fetch(`${BASE}/api/weather?city=${encodeURIComponent(city)}&days=5`);
    const data = await res.json();
    if (!data.ok) throw new Error(data.error.message);
    return data;
  }

  const wx = await getWeather("Warsaw");
  console.log(wx.location.name, wx.current.temperature, wx.units.temperature);
  console.log(wx.current.condition, "-", wx.current.description);

  for (const day of wx.daily) {
    console.log(day.date, day.condition, `${day.temp_min}..${day.temp_max}`);
  }

  // by coordinates
  const local = await fetch(`${BASE}/api/weather?lat=48.8566&lon=2.3522`).then(r => r.json());

  // CORS is open, so this also works straight from a static page:
  // <script>fetch("BASE/api/current?city=Berlin").then(...)</script>
  import requests

  BASE = "http://localhost:3000"

  def get_weather(city=None, lat=None, lon=None, days=7, units="metric"):
      params = {"days": days, "units": units}
      if city:
          params["city"] = city
      else:
          params.update({"lat": lat, "lon": lon})
      r = requests.get(f"{BASE}/api/weather", params=params, timeout=15)
      r.raise_for_status()
      data = r.json()
      if not data["ok"]:
          raise RuntimeError(data["error"]["message"])
      return data

  wx = get_weather(city="Warsaw")
  cur = wx["current"]
  print(f"{wx['location']['name']}: {cur['temperature']}{wx['units']['temperature']} "
        f"({cur['condition']}, feels {cur['apparent_temperature']})")

  for day in wx["daily"]:
      print(day["date"], day["condition"], day["temp_min"], "..", day["temp_max"])

  # coordinates + imperial
  wx_us = get_weather(lat=40.7128, lon=-74.0060, units="imperial", days=3)
  RATE LIMIT
    60 requests per 60 seconds per IP address (configurable via RATE_LIMIT_MAX).
    Every response carries:
      X-RateLimit-Limit       requests allowed in the window
      X-RateLimit-Remaining   requests left
      X-RateLimit-Reset       seconds until the window resets
      Retry-After             only on 429

  CACHING
    Weather responses are cached in memory for 10 minutes, keyed by
    lat/lon (rounded to 2 decimals), days, units, timezone and hours.
    Geocoding results are cached for 24 hours.
    X-Cache: HIT | MISS tells you which path served your call, and
    meta.cache_age_seconds reports how old the cached payload is.
    This keeps Open-Meteo unstressed - please do not disable it.

  ERROR CODES
    400 missing_parameter   neither city nor lat+lon supplied
    400 bad_request         parameter out of range or too long
    404 not_found           the city could not be geocoded
    404 unknown_endpoint    no such path under /api
    429 rate_limited        too many requests, see Retry-After
    502 upstream_error      Open-Meteo returned an error
    504 upstream_timeout    Open-Meteo did not answer in 12 s
    500 internal_error      unexpected server failure

  FAIR USE
    Free for personal and commercial projects. Keep client side caching on,
    do not poll faster than once per 10 minutes for the same coordinates and
    credit Open-Meteo (CC BY 4.0) where you show the data.
GET
response will appear here ...