This repository is a full-stack weather dashboard: a Flask backend talks to OpenWeatherMap for live conditions and daily-style forecasts, and a Vue 3 + Vite + Tailwind frontend lets you search by city, see current stats (temp, humidity, wind, and more), and browse up to six future days with labeled highs and lows and local SVG weather art. The stack also includes server-side caching, rate limiting, structured logging, source attribution, unit preferences, recent cities, and optional geolocation (coordinates go to the backend; your OWM key stays on the server).
weather_forecast/
├── backend/ # Flask API
├── weather-frontend/ # Vue 3 + Vite + Tailwind CSS
├── docker-compose.yml # optional: build/run backend + frontend containers
└── compose.env.example
- Python 3.x, Flask, flask-cors, Flask-Limiter, requests, python-dotenv
-
Create and activate a virtual environment (from
backend/):python -m venv venv
Windows:
venv\Scripts\activate
macOS/Linux:source venv/bin/activate -
Install dependencies:
pip install -r requirements.txt
-
Copy
backend/.env.exampletobackend/.envand set at least:WEATHER_API_KEY=your_openweathermap_api_key
Do not commit
.env. Optional variables are documented in.env.example(WEATHER_CACHE_TTL,CORS_ORIGINS,RATE_LIMIT_WEATHER,LOG_LEVEL,SECRET_KEY).
From backend/:
python run.pyDefault URL: http://127.0.0.1:5000
GET /api/weatheraccepts eithercity(trimmed, non-empty) orlat+lon(floats in valid ranges). Iflat/lonare partially sent or invalid, the API returns 400 with{"error": "Invalid latitude or longitude"}. If neither mode is satisfied, 400 with{"error": "City is required, or provide lat and lon"}.- Cache — The handler looks up an in-memory entry keyed by
city:<lowercase city>orgeo:<lat rounded 4dp>:<lon rounded 4dp>. If found and younger thanWEATHER_CACHE_TTL(default 300 seconds), the handler returns the stored JSON immediately withmeta.cache_hit: true. In that casemeta.fetched_atis the original UTC time when that entry was first stored (not “now”). - Upstream — On a miss, it calls OpenWeatherMap
/weatherthen/forecast(same query: city name or lat/lon). If the forecast call returns an error object without alist, the code still continues with an empty list (forecast can be rebuilt from Open-Meteo only). - Assembly —
format_weathershapes current from OWM.build_six_day_forecast(informatter.py) builds six local-calendar days starting tomorrow (today is excluded): it aggregates OWM’s 3-hourlistby date using the location’stimezoneoffset, then fills any missing dates using Open-Meteo daily data atraw_current["coord"](no extra API key). WMO codes are mapped to OWM-likecondition/description/iconfor the frontend art. - Success JSON — The handler stores
{ fetched_at, current, forecast }in the cache and returns it withmeta(sources,note,cache_hit: false). - Errors — For upstream/config failures, responses are
{"error": "..."}(anderror_type:"config"is stripped before sending). These error bodies do not includemeta,current, orforecast. - Rate limiting — Flask-Limiter applies only to
GET /api/weather(not/api/healthor/api/test). Default limit:60 per minuteper client IP (RATE_LIMIT_WEATHER).
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
JSON health payload (status, service name, UTC time). Not rate-limited. |
| GET | /api/test |
Simple “backend working” message. Not rate-limited. |
| GET | /api/weather?city=<name> |
Current + forecast. city required unless lat/lon are used. Rate-limited. |
| GET | /api/weather?lat=<n>&lon=<n> |
Same as above using coordinates (browser geolocation flow). Rate-limited. |
HTTP status summary
| Status | When |
|---|---|
200 |
Success: meta, current, forecast. |
400 |
Missing/invalid city or invalid lat/lon. |
404 |
OWM could not resolve the city or coordinates. |
429 |
Per-IP rate limit exceeded (RATE_LIMIT_WEATHER). |
503 |
WEATHER_API_KEY missing or empty. |
meta (success only)
| Field | Meaning |
|---|---|
fetched_at |
ISO UTC timestamp when this cache entry was first stored (unchanged on cache hits). |
sources |
Static attribution: OpenWeatherMap + Open-Meteo with id, label, url, role. |
cache_hit |
true if served from TTL cache. |
note |
Fixed copy explaining six days from tomorrow and when Open-Meteo supplements OWM. |
current fields (always metric: °C, m/s)
city, country, temperature, feels_like, humidity, condition, description, icon, wind_speed.
Each forecast[] item
date (ISO date), weekday, label, temp_min, temp_max, condition, description, icon, humidity, wind_speed.
The frontend treats these numbers as metric and converts only in the UI when the user picks °F or mph.
Success example (shape)
{
"meta": {
"fetched_at": "2026-03-23T12:00:00+00:00",
"sources": [
{ "id": "openweathermap", "label": "OpenWeatherMap", "url": "https://openweathermap.org/", "role": "Current weather and 5-day / 3-hour forecast" },
{ "id": "open-meteo", "label": "Open-Meteo", "url": "https://open-meteo.com/", "role": "Daily values used to complete the 6-day outlook when needed" }
],
"cache_hit": false,
"note": "Six-day outlook starts tomorrow (today excluded). Open-Meteo may supply later days when the free OWM window ends."
},
"current": {
"city": "…",
"country": "…",
"temperature": 0,
"feels_like": 0,
"humidity": 0,
"condition": "…",
"description": "…",
"icon": "…",
"wind_speed": 0
},
"forecast": [
{
"date": "YYYY-MM-DD",
"weekday": "Mon",
"label": "Mar 24",
"temp_min": 0,
"temp_max": 0,
"condition": "…",
"description": "…",
"icon": "…",
"humidity": 0,
"wind_speed": 0
}
]
}backend/
├── app/
│ ├── routes/weather_routes.py
│ ├── services/weather_service.py
│ ├── services/cache.py
│ ├── utils/formatter.py
│ ├── utils/open_meteo_daily.py
│ ├── extensions.py # Flask-Limiter
│ ├── config.py
│ └── __init__.py
├── run.py
├── requirements.txt
├── .env.example
└── .env # you create this
- Vue 3, Vite, axios, Tailwind CSS
From weather-frontend/:
npm installCopy weather-frontend/.env.example to .env if you need a non-default API URL.
Development server:
npm run devProduction build:
npm run build
npm run previewSet VITE_API_BASE_URL (see .env.example) to point at your deployed API; default is http://127.0.0.1:5000/api (trailing slash is stripped in code).
src/services/api.js— Axios instance withbaseURLfromVITE_API_BASE_URLor the localhost default.getWeather(city)→GET /weather?city=….getWeatherByCoords(lat, lon)→GET /weather?lat=…&lon=….main.ts— CallsinitTheme()before mounting the app so the correct light/dark class is on<html>on first paint.tailwind.config.jsusesdarkMode: 'class';style.csssetscolor-schemefor native controls.App.vue— Search submitsgetWeather; “Use my location” usesnavigator.geolocationthengetWeatherByCoords. On success it keepsbundle= full JSON (meta,current,forecast).addRecentCityruns after a successful city search or after geolocation whencurrent.cityexists (recent list inlocalStorageviautils/recentCities.js). Theme (Light / Dark / System) is persisted withutils/preferences.tsand applied viautils/theme.ts(setThemeModeupdateslocalStorageand toggles thedarkclass).- Units — API values stay metric;
WeatherCardandForecastSectionuseutils/displayUnits.jswith preferences fromutils/preferences.ts(°C/°F, m/s/mph). - Illustrations —
utils/weatherArt.jsmaps OWM-stylecondition/icon/descriptionto SVGs undersrc/assets/weather/, rendered byWeatherIllustration.vue. - Footer —
AppFooter.vuereadsbundle.metawhen present (last updated, cache hint, sources); before the first load it still shows the same source list and note from static defaults in that component. - Errors — Axios
429shows a dedicated “too many requests” message; other failures useresponse.data.errorwhen present.aria-liveannounces load/error status for assistive tech.
- Light / dark / system theme —
class-based Tailwind dark mode; choice stored inlocalStorage(wf_color_mode).initTheme()inmain.tsapplies thedarkclass on<html>and listens for OS changes when mode is System. - Unit toggles — °C / °F and m/s / mph (stored in
localStorage). - Recent cities — quick chips under the search field.
- Use my location — browser geolocation →
GET /api/weather?lat=&lon=. - Attribution footer — data sources, explanatory note, last updated time, cache hint.
- Accessibility — labeled search,
aria-livestatus region,aria-pressedon unit toggles, landmark headings.
weather-frontend/
├── src/
│ ├── components/ # App.vue, SearchBar, WeatherCard, ForecastSection, PreferencesBar, AppFooter, WeatherIllustration, …
│ ├── services/api.js
│ ├── utils/weatherArt.js
│ ├── utils/preferences.ts
│ ├── utils/theme.ts
│ ├── utils/recentCities.js
│ ├── utils/displayUnits.js
│ ├── assets/weather/
│ ├── App.vue
│ ├── main.ts
│ └── style.css
├── vite.config.js
├── postcss.config.js
├── tailwind.config.js
├── .env.example
└── package.json
- Terminal A —
backend/: activate venv,python run.py - Terminal B —
weather-frontend/:npm run dev - Open the Vite URL (printed in the terminal, usually
http://localhost:5173)
For stricter CORS in production, set CORS_ORIGINS on the backend to your frontend origin(s).
From the repository root (where docker-compose.yml lives):
docker compose build
docker compose up- UI:
http://localhost:8080orhttp://127.0.0.1:8080(or the host port you set withFRONTEND_PORT). - API:
http://127.0.0.1:5000/apiby default (orBACKEND_PORT). The frontend bundle calls whatever you baked in withVITE_API_BASE_URL.
Set WEATHER_API_KEY in weather-backend/.env (copy from weather-backend/.env.example). Docker Compose loads that file into the backend container, so use the same file as for local python run.py.
Optional: a .env next to docker-compose.yml (see compose.env.example) for BACKEND_PORT, FRONTEND_PORT, VITE_API_BASE_URL, etc. Do not add an empty WEATHER_API_KEY= there — it would override the key inside the container.
Other useful variables (all optional unless noted):
BACKEND_PORT— host port for the API (default5000).VITE_API_BASE_URL— build arg for the static UI; must be reachable from the browser. If you change the API port, update this and rundocker compose build --no-cache frontend.FRONTEND_PORT— host port for nginx (default8080).CORS_ORIGINS— comma-separated origins if you want strict CORS instead of the dev default; include your UI origin(s) when locking this down.
The frontend service waits until the backend /api/health check passes before starting.
Files: docker-compose.yml, weather-backend/Dockerfile, weather-backend/.dockerignore, weather-frontend/Dockerfile, weather-frontend/nginx.conf, weather-frontend/.dockerignore.
- If Open-Meteo is unreachable, you may get fewer than six forecast cards; the UI notes when that happens.
- Keep API keys only in
weather-backend/.env. Set a strongSECRET_KEYin production. - Rate limits and cache reduce load on OpenWeatherMap; tune
WEATHER_CACHE_TTLandRATE_LIMIT_WEATHERas needed.