Search a city, see its current weather — that's the entire brief. No accounts, no forecasts, no backend, no API key to protect (Open-Meteo is free and keyless). What the project is actually built to demonstrate is discipline: five small modules, each with exactly one job, none of them reaching into another's responsibility.
What makes it work
-
Every technical failure becomes one friendly sentence.
A dropped connection, a bad response, unexpected data — all caught in
api.jsand turned into "Unable to connect. Please check your internet connection and try again." before it ever reaches the rest of the app. A city that genuinely isn't found gets its own distinct message, since that's a valid result, not a technical failure. - A real race condition, found and fixed. Clicking two recent-search chips in quick succession could show the wrong city, since nothing stopped two searches from overlapping. Fixed by tracking in-flight state and disabling the search button, input, and chips together while a search is running.
Design at a glance
- bg · #eef2f7
- surface · #ffffff
- accent · #2f6fed
- text · #1c2331
System font stack throughout, mobile-first with breakpoints at 600px and 900px.
Built to last
- Four single-responsibility modules (
api.js,weatherFormatter.js,storage.js,app.js) — none touches another's job, and onlyapp.jstouches the DOM - Recent searches persist via
localStorage, capped at 5, case-insensitive de-duplication - Last-searched city restores automatically on reload; a genuine EN/FR toggle throughout
For developers — the full technical breakdown
01. Strict separation of concerns, enforced end-to-end
- Problem
- Small frontend-only apps tend to let API calls, formatting, storage, and DOM updates blur together in one file as they grow.
- Choice
- Four modules, each with exactly one job:
api.jstalks to Open-Meteo and nothing else,weatherFormatter.jsturns data into display strings with no DOM or network access,storage.jsonly reads/writeslocalStorage, andapp.jsis the sole file allowed to touch the DOM. - Result
- Any of the three non-DOM modules could be unit-tested or swapped independently — none of them needs a browser to run.
02. A found-in-testing race condition between overlapping searches
- Problem
- Clicking a second recent-search chip before the first search resolved could let the earlier request's response land last, showing the wrong city.
- Choice
- Added an in-progress flag that disables the search button, input, and all recent-search chips together for the duration of a request.
- Result
- Two overlapping searches can no longer race each other — verified by deliberately firing rapid successive searches during the production-polish milestone.
Accessibility & performance
- All API/city data inserted via
textContent, neverinnerHTML— no injection path from a city name or API response :focus-visiblestates on the language toggle; visible focus throughoutprefers-reduced-motionrespected for state transitions- Defensive
localStoragereads — corrupted or tampered data falls back to an empty list instead of crashing the app
What I’d do differently
No forecast, only current conditions — a deliberate scope cut to keep the separation-of-concerns demonstration tight. A v2 forecast view would need a fifth module rather than growing weatherFormatter.js past its one job.
Stack
- Vanilla JavaScript (ES modules)
- Open-Meteo API (geocoding & weather, keyless)
- localStorage