Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
35 minLesson 34 of 35
Final Projects

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):

  1. Sign up at openweathermap.org
  2. Get your API key from the dashboard
  3. Store it in a .env file (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 fetchingPromise.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 transformationgroupForecastByDay() 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

Get Notes Free →
!