AiTechWorlds
AiTechWorlds
Build a weather app that fetches real time data from a public API, displays current conditions and a 5-day forecast, and handles loading states, errors, and geolocation. This project is the quintessential fetch API exercise.
┌─────────────────────────────┐
│ 🔍 [Search city...] [📍] │
├─────────────────────────────┤
│ New York │
│ ⛅ Partly Cloudy │
│ 72°F Feels like 69°F │
│ 💧 45% 💨 12 mph 👁 10mi │
├─────────────────────────────┤
│ 5-Day Forecast │
│ Mon Tue Wed Thu Fri │
│ 🌤 🌧 ⛅ 🌤 🌥 │
│ 75° 63° 70° 78° 72° │
└─────────────────────────────┘Use the OpenWeatherMap API (free tier: 1,000 calls/day):
.env file (never commit it!)# .env
VITE_WEATHER_API_KEY=your_key_hereweather-app/
├── index.html
├── style.css
├── src/
│ ├── api.js # API calls
│ ├── ui.js # DOM manipulation
│ ├── geolocation.js
│ └── main.js # entry pointconst API_KEY = import.meta.env.VITE_WEATHER_API_KEY;
const BASE = "https://api.openweathermap.org/data/2.5";
async function apiGet(endpoint, params) {
const url = new URL(`${BASE}/${endpoint}`);
url.searchParams.set("appid", API_KEY);
url.searchParams.set("units", "imperial"); // or "metric"
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const response = await fetch(url);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message ?? `Weather API error ${response.status}`);
}
return response.json();
}
export async function getCurrentWeather(city) {
return apiGet("weather", { q: city });
}
export async function getCurrentWeatherByCoords(lat, lon) {
return apiGet("weather", { lat, lon });
}
export async function getForecast(city) {
const data = await apiGet("forecast", { q: city, cnt: 40 });
return groupForecastByDay(data.list);
}
function groupForecastByDay(list) {
const days = {};
list.forEach(item => {
const date = new Date(item.dt * 1000);
const key = date.toLocaleDateString("en-US", { weekday: "short" });
if (!days[key]) {
days[key] = { day: key, temps: [], icon: item.weather[0].icon, desc: item.weather[0].description };
}
days[key].temps.push(item.main.temp);
});
return Object.values(days).slice(0, 5).map(day => ({
...day,
high: Math.round(Math.max(...day.temps)),
low: Math.round(Math.min(...day.temps))
}));
}
export function getIconUrl(icon) {
return `https://openweathermap.org/img/wn/${icon}@2x.png`;
}export function getLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error("Geolocation is not supported"));
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
(err) => reject(new Error(err.message)),
{ timeout: 10000 }
);
});
}export function showLoading() {
document.getElementById("weather-display").innerHTML = `
<div class="loading">
<div class="spinner"></div>
<p>Loading weather data...</p>
</div>
`;
}
export function showError(message) {
document.getElementById("weather-display").innerHTML = `
<div class="error">
<span class="error-icon">⚠️</span>
<p>${message}</p>
</div>
`;
}
export function displayWeather(current, forecast) {
const { name, main, weather, wind, visibility } = current;
const condition = weather[0];
document.getElementById("weather-display").innerHTML = `
<div class="current-weather">
<div class="location-name">${name}</div>
<div class="condition-row">
<img src="https://openweathermap.org/img/wn/${condition.icon}@2x.png"
alt="${condition.description}" class="weather-icon">
<div class="temp">${Math.round(main.temp)}°F</div>
</div>
<div class="description">${capitalize(condition.description)}</div>
<div class="feels-like">Feels like ${Math.round(main.feels_like)}°F</div>
<div class="details">
<div class="detail">💧 ${main.humidity}%</div>
<div class="detail">💨 ${Math.round(wind.speed)} mph</div>
<div class="detail">👁 ${Math.round(visibility / 1609)} mi</div>
<div class="detail">🌡 ${Math.round(main.temp_min)}° / ${Math.round(main.temp_max)}°</div>
</div>
</div>
<div class="forecast">
<h3>5-Day Forecast</h3>
<div class="forecast-grid">
${forecast.map(day => `
<div class="forecast-day">
<div class="day-name">${day.day}</div>
<img src="https://openweathermap.org/img/wn/${day.icon}.png" alt="${day.desc}">
<div class="temp-range">
<span class="high">${day.high}°</span>
<span class="low">${day.low}°</span>
</div>
</div>
`).join("")}
</div>
</div>
`;
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}import { getCurrentWeather, getCurrentWeatherByCoords, getForecast } from "./api.js";
import { getLocation } from "./geolocation.js";
import { showLoading, showError, displayWeather } from "./ui.js";
const searchInput = document.getElementById("search-input");
const searchBtn = document.getElementById("search-btn");
const locationBtn = document.getElementById("location-btn");
async function loadWeather(city) {
showLoading();
try {
const [current, forecast] = await Promise.all([
getCurrentWeather(city),
getForecast(city)
]);
displayWeather(current, forecast);
// Save last searched city
localStorage.setItem("last-city", city);
} catch (err) {
showError(err.message === "city not found"
? `"${city}" not found. Try another city name.`
: "Failed to load weather data. Please try again."
);
}
}
async function loadWeatherByLocation() {
showLoading();
try {
const { lat, lon } = await getLocation();
const current = await getCurrentWeatherByCoords(lat, lon);
const forecast = await getForecast(current.name);
displayWeather(current, forecast);
} catch (err) {
showError(`Location error: ${err.message}`);
}
}
// Search
searchBtn.addEventListener("click", () => {
const city = searchInput.value.trim();
if (city) loadWeather(city);
});
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
const city = searchInput.value.trim();
if (city) loadWeather(city);
}
});
// Geolocation
locationBtn.addEventListener("click", loadWeatherByLocation);
// Load last city or default
const lastCity = localStorage.getItem("last-city") ?? "New York";
searchInput.value = lastCity;
loadWeather(lastCity);Parallel fetching Promise.all([getCurrentWeather(), getForecast()]) fetches both simultaneously, cutting load time in half.
Error classification distinguishing "city not found" from network errors gives users helpful messages.
State persistence localStorage remembers the last searched city across sessions.
Loading states the UI updates through loading → success/error to prevent jarring jumps.
Data transformation groupForecastByDay() converts raw API data (3-hour intervals) into the shape the UI needs.
Next project: Budget Tracker building a full data management app with charts.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises