Weather Dashboard App
Build a weather dashboard with city search, forecasts, and saved recent searches
Time to implement the project: ~ 12-20 hours
- HTML
- CSS
- Responsive UI
- JavaScript
- Fetch API
- Async / Await
- Local Storage
In this project, you will build a weather dashboard that lets users search by city and instantly see current conditions plus a short forecast. The UI must include a search input, a clear “current weather” panel, and a forecast section that displays multiple upcoming time blocks or days with temperature and conditions. Your app must handle loading, empty states, and “city not found” errors without breaking the layout.
Every successful search must be stored in localStorage and displayed as a recent-search list that users can click to reload weather data for that city. The
list should update in real time, avoid duplicates, and keep a sensible limit. The dashboard must remain readable on mobile and desktop, with consistent spacing and
predictable content formatting.
What This App Teaches in a Real Workflow
This project trains the full frontend loop: accept input, fetch data, transform it into UI-friendly output, and store user history for a better experience. You will practice building stable request logic, rendering conditional states, and keeping the interface consistent while data changes.
Weather dashboards reflect a common product pattern: search-driven pages with API responses and persistent user preferences. Completing this task shows that you can deliver a practical interface that behaves reliably across refreshes and repeated use.
Baseline Skills Required
You should already write basic HTML/CSS layouts and feel comfortable with JavaScript functions and events. This project assumes you can call an API endpoint, read JSON, and update the DOM without frameworks.
- HTML forms and input handling
- CSS layout fundamentals and responsive spacing
- JavaScript events (submit, click) and DOM updates
- Fetch API with async/await and error handling
- Working with localStorage and JSON serialization
- Basic data formatting (units, dates, labels)
Acceptance Criteria for a Production-Style Dashboard
A credible dashboard focuses on correctness and user trust: searches return the right city, the forecast stays readable, and the app communicates errors without confusion. These requirements mirror how entry-level frontend work is evaluated - state handling, predictable UI updates, and persistence that survives refreshes.
| Requirement | Explanation |
| City search with input validation | Validation prevents wasted requests and keeps UI feedback clean when input is empty or invalid. |
| Current weather panel | A clear current-state section organizes key data and sets the baseline for the rest of the dashboard. |
| Forecast section with multiple entries | A structured forecast proves you can map API data into repeatable UI components. |
| Loading and error states | Reliable messaging builds user trust and prevents “blank UI” confusion during network issues. |
| Recent searches stored in localStorage | Persistence improves usability and demonstrates practical state storage between sessions. |
| Clickable recent-search items | Re-running searches from history shows you can connect stored state to new API requests. |
| Duplicate handling and list limits | A clean history list stays useful and prevents localStorage from turning into noisy clutter. |
| Responsive layout for mobile | Dashboards must remain readable on small screens, with cards stacking and text staying legible. |
Build Tips That Prevent Common Bugs
Treat data handling and UI rendering as separate responsibilities. First, build a request layer that returns a normalized object: city name, current values, and an array of forecast entries. Then render from that model, replacing the UI cleanly on each search. For recent searches, store a small list and update it only after successful responses to keep history accurate. When debugging, watch network responses in DevTools and verify that your UI never depends on optional fields. When your data model stays consistent, the interface stays stable.
- Use a single render function for forecast items so formatting stays consistent across entries
- Show a loading state immediately to prevent repeated submits and “nothing happened” confusion
- Store recent searches as an array and always deduplicate before saving
- Limit history length (for example 5-10 cities) to keep the UI focused and storage clean
Common Weather Dashboard Mistakes and How to Fix Them
1. Sending API requests without validating the city input
One of the most common mistakes in weather dashboard projects is sending a request every time the user submits the form, even when the input is empty or contains only spaces. This wastes API calls, creates confusing error messages, and can quickly become a problem when using a weather API with request limits.
Problematic approach:
form.addEventListener('submit', async (event) => {
event.preventDefault();
const city = cityInput.value;
const weather = await getWeather(city);
renderWeather(weather);
});
This code sends a request even if cityInput.value is empty, contains accidental spaces, or has not changed from the previous search.
Better approach:
form.addEventListener('submit', async (event) => {
event.preventDefault();
const city = cityInput.value.trim();
if (!city) {
showError('Please enter a city name before searching.');
return;
}
clearError();
await searchWeather(city);
});
Pay attention to: Always trim and validate user input before calling the API. A weather dashboard should not rely on the API to handle every avoidable user mistake. Good validation improves user experience and protects your request quota.
2. Treating every fetch response as successful
Beginners often assume that fetch() will throw an error whenever something goes wrong. In reality, fetch() only rejects for network-level
failures. If the API returns 404 for a city not found or 401 for an invalid API key, the request may still resolve, but
response.ok will be false.
Problematic code:
async function getWeather(city) {
const response = await fetch(apiUrl + city);
const data = await response.json();
return data;
}
This can pass an error response into your rendering function, causing broken cards, missing values, or confusing text like undefined in the UI.
Recommended solution:
async function getWeather(city) {
const response = await fetch(buildWeatherUrl(city));
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Weather data could not be loaded.');
}
return data;
}
Usage with clear error handling:
async function searchWeather(city) {
try {
showLoading();
const data = await getWeather(city);
renderWeather(normalizeWeatherData(data));
saveRecentSearch(city);
} catch (error) {
showError(error.message);
} finally {
hideLoading();
}
}
Pay attention to: Always check response.ok. Your app should have different UI states for loading, success, empty result, city not found,
invalid API key, and network failure.
3. Rendering raw API data directly without normalizing it first
Weather APIs often return deeply nested objects. If your UI reads those fields directly everywhere, the code becomes fragile. One missing field can break the whole dashboard, and switching API providers later becomes much harder.
Fragile rendering:
temperature.textContent = data.main.temp + '°C';
humidity.textContent = data.main.humidity + '%';
wind.textContent = data.wind.speed + ' m/s';
description.textContent = data.weather[0].description;
This works only when every nested field exists exactly as expected.
Better normalized model:
function normalizeCurrentWeather(data) {
return {
city: data.name,
temperature: Math.round(data.main?.temp),
feelsLike: Math.round(data.main?.feels_like),
humidity: data.main?.humidity,
windSpeed: data.wind?.speed,
description: data.weather?.[0]?.description || 'No description available',
icon: data.weather?.[0]?.icon
};
}
Then render from your own model:
function renderCurrentWeather(weather) {
cityName.textContent = weather.city;
temperature.textContent = `${weather.temperature}°`;
feelsLike.textContent = `Feels like ${weather.feelsLike}°`;
humidity.textContent = `${weather.humidity}%`;
wind.textContent = `${weather.windSpeed} m/s`;
description.textContent = weather.description;
}
Pay attention to: Create a clean internal data model before rendering. Your components should not need to understand every detail of the external API response. This makes the dashboard easier to debug, extend, and refactor.
4. Saving recent searches before confirming the city exists
Recent search history should represent successful searches. If you save the city name before the API confirms it, your history can fill up with misspelled cities, failed requests, empty strings, or duplicate entries. This makes the feature less useful.
Problematic approach:
async function searchWeather(city) {
saveRecentSearch(city);
const data = await getWeather(city);
renderWeather(data);
}
If the request fails, the bad city is already saved.
Better approach:
async function searchWeather(city) {
try {
showLoading();
const data = await getWeather(city);
const weather = normalizeCurrentWeather(data);
renderCurrentWeather(weather);
saveRecentSearch(weather.city);
} catch (error) {
showError(error.message);
} finally {
hideLoading();
}
}
Deduplicate and limit history:
function saveRecentSearch(city) {
const normalizedCity = city.trim();
const searches = loadRecentSearches();
const updatedSearches = [
normalizedCity,
...searches.filter((item) => item.toLowerCase() !== normalizedCity.toLowerCase())
].slice(0, 6);
localStorage.setItem('weatherRecentSearches', JSON.stringify(updatedSearches));
renderRecentSearches(updatedSearches);
}
Pay attention to: Save searches only after successful responses. Use the city name returned by the API when possible, because it is usually better formatted than raw user input.
5. Exposing API keys carelessly in frontend code
Many beginner weather projects use API keys directly inside JavaScript files. For learning projects, this is common, but it is important to understand the limitation: anything shipped to the browser can be seen by users. You should never place private production secrets in frontend code.
Common beginner setup:
const API_KEY = 'your_real_api_key_here';
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}`;
This key is visible in browser DevTools and in your public GitHub repository if committed.
Safer learning-project approach:
const API_KEY = 'YOUR_API_KEY_HERE';
// Do not commit a real production key.
// Use a restricted free-tier key only for practice projects.
Better production-style direction:
// Frontend
const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
// Backend or serverless function
// Reads the real API key from environment variables
// and calls the weather provider securely.
Pay attention to: For a static beginner project, use a restricted test key and do not commit sensitive credentials. For a real deployed app, move weather requests through a backend or serverless function so the API key can be stored in environment variables.
6. Allowing outdated requests to overwrite newer search results
If a user searches for several cities quickly, an older request may finish after a newer one. Without protection, the app can display the wrong city because the slower earlier response overwrites the latest search result.
Problematic flow:
async function searchWeather(city) {
showLoading();
const data = await getWeather(city);
renderWeather(data);
hideLoading();
}
If the user searches “London” and then immediately searches “Berlin”, the London response may arrive last and replace the Berlin dashboard.
Better approach with request tracking:
let latestRequestId = 0;
async function searchWeather(city) {
const requestId = ++latestRequestId;
try {
showLoading();
const data = await getWeather(city);
if (requestId !== latestRequestId) {
return;
}
renderWeather(normalizeCurrentWeather(data));
} catch (error) {
if (requestId === latestRequestId) {
showError(error.message);
}
} finally {
if (requestId === latestRequestId) {
hideLoading();
}
}
}
Pay attention to: Weather dashboards are search-driven interfaces. Protect the UI from race conditions by tracking the latest request, disabling
repeated submits during loading, or using AbortController to cancel previous requests.
By completing this project, you'll gain practical experience building a search-driven dashboard that fetches live weather data, renders current conditions and forecasts, and stores recent searches in localStorage for a smoother repeat experience. This foundation strengthens your skills in async UI updates, error handling, data formatting, and persistence - core abilities used in real frontend applications and technical interviews.
Reference Weather Dashboard Implementations
Full weather dashboard reference:
omar-mazen - Weatherio
This repository is useful because it shows a more complete weather dashboard experience built with HTML, CSS, JavaScript, and the OpenWeather API. The project includes current weather, a 5-day forecast, wind speed, daily temperature, sunrise and sunset, air quality, humidity, visibility, feels-like temperature, and atmospheric pressure. :contentReference[oaicite:1]{index=1}
What to study in the code:
- How the dashboard organizes many weather metrics without making the UI unreadable.
- How current weather and forecast data are separated visually.
- How API data is mapped into different cards and dashboard sections.
- How the app handles city-based weather searches.
- How you could simplify the same ideas for a beginner-level implementation.
Use this repository as a reference for feature depth. Do not try to copy every metric at once; first build a stable current weather panel and forecast section, then add extra cards only when the request and rendering flow are reliable.
Vanilla HTML/CSS/JS dashboard example:
Alapatihimaja23 - Weather Dashboard
This repository is valuable because it matches the stack of this project closely: vanilla HTML, CSS, and JavaScript. The project describes current weather display, 5-day forecasts, location services, unit toggle between Celsius and Fahrenheit, search history, responsive design, and a clean UI with weather icons and animations. :contentReference[oaicite:2]{index=2}
Pay attention to:
- How the project structures
index.html,style.css, andscript.js. - How current weather and forecast sections are rendered.
- How search history is stored and reused for quick city searches.
- How unit switching affects all displayed temperature values.
- How responsive design keeps the dashboard usable across desktop, tablet, and mobile screens.
This is a strong implementation to compare with your own dashboard because it includes several realistic product features while still staying close to beginner-friendly frontend technologies.
Compact weather widget reference:
platevoltage - Weather Widget
This repository is useful as an alternative implementation because it focuses on a simpler weather widget experience. It uses the OpenWeatherMap API to show current weather and a five-day forecast, uses Moment.js for time and date functions, and stores recent searches in localStorage so the last search can be restored when the app launches. :contentReference[oaicite:3]{index=3}
What to compare with your own project:
- How a smaller widget decides which weather data is essential.
- How recent searches are stored and restored with localStorage.
- How date and time formatting is handled for forecast entries.
- How current conditions and forecast cards are presented in a compact layout.
- How the project could be modernized by replacing Moment.js with native date formatting or a lighter utility.
A useful exercise is to compare this widget with a larger dashboard and decide which features belong in your own project. A focused, reliable weather dashboard is better than a crowded interface with unstable API handling.