Project: Weather App with API
Project: Weather App with API
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.
What You'll Build
┌─────────────────────────────┐
│ 🔍 [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° │
└─────────────────────────────┘
Setup: API Key
Use the OpenWeatherMap API (free tier: 1,000 calls/day):
- Sign up at openweathermap.org
- Get your API key from the dashboard
- Store it in a
.envfile (never commit it!)
# .env
VITE_WEATHER_API_KEY=your_key_here
Project Structure
weather-app/
├── index.html
├── style.css
├── src/
│ ├── api.js # API calls
│ ├── ui.js # DOM manipulation
│ ├── geolocation.js
│ └── main.js # entry point
api.js
const 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`;
}
geolocation.js
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 }
);
});
}
ui.js
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);
}
main.js
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);
Key Patterns Demonstrated
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