Weather App

Real API integration. Fetch live data, handle errors gracefully, and build dynamic UI.

API Setup

Use OpenWeatherMap's free tier — 1000 calls/day.

// 1. Sign up at openweathermap.org/api
// 2. Get your free API key
// 3. Store it securely (never commit to git!)

// Project structure:
// weather-app/
// ├── index.html
// ├── style.css
// ├── app.js
// └── config.js  ← API key (gitignored)

// config.js:
const CONFIG = {
  API_KEY: "your_api_key_here",
  BASE_URL: "https://api.openweathermap.org/data/2.5",
};

// .gitignore:
// config.js

// API endpoints we'll use:
// Current weather: /weather?q={city}&appid={key}&units=metric
// 5-day forecast:  /forecast?q={city}&appid={key}&units=metric
// By coordinates:  /weather?lat={lat}&lon={lon}&appid={key}

// HTML structure:
// <div class="app">
//   <form class="search">
//     <input type="text" placeholder="Search city...">
//     <button type="submit">🔍</button>
//   </form>
//   <button class="location-btn">📍 My Location</button>
//   <div class="weather-display hidden">...</div>
//   <div class="error hidden"></div>
//   <div class="loading hidden">Loading...</div>
// </div>

Fetching Data

async/await with proper error handling for API calls.

// Core fetch function with error handling:
async function fetchWeather(city) {
  showLoading();
  hideError();

  try {
    const url = `${CONFIG.BASE_URL}/weather?q=${encodeURIComponent(city)}&appid=${CONFIG.API_KEY}&units=metric`;
    const response = await fetch(url);

    if (!response.ok) {
      if (response.status === 404) throw new Error("City not found");
      if (response.status === 401) throw new Error("Invalid API key");
      throw new Error("Failed to fetch weather data");
    }

    const data = await response.json();
    displayWeather(data);
  } catch (error) {
    showError(error.message);
  } finally {
    hideLoading();
  }
}

// Fetch 5-day forecast:
async function fetchForecast(city) {
  const url = `${CONFIG.BASE_URL}/forecast?q=${encodeURIComponent(city)}&appid=${CONFIG.API_KEY}&units=metric`;
  const response = await fetch(url);
  if (!response.ok) throw new Error("Forecast unavailable");
  return response.json();
}

Always encode user input in URLs (encodeURIComponent). Check response.ok before parsing. Use finally for cleanup.

1 / 2

Geolocation

Get weather for the user's current location.

// Geolocation API — get user's coordinates:
const locationBtn = document.querySelector(".location-btn");

locationBtn.addEventListener("click", () => {
  if (!navigator.geolocation) {
    showError("Geolocation not supported in this browser");
    return;
  }

  showLoading();

  navigator.geolocation.getCurrentPosition(
    // Success:
    async (position) => {
      const { latitude, longitude } = position.coords;
      await fetchWeatherByCoords(latitude, longitude);
    },
    // Error:
    (error) => {
      hideLoading();
      switch (error.code) {
        case error.PERMISSION_DENIED:
          showError("Location permission denied");
          break;
        case error.POSITION_UNAVAILABLE:
          showError("Location unavailable");
          break;
        case error.TIMEOUT:
          showError("Location request timed out");
          break;
      }
    },
    // Options:
    { enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 }
  );
});

async function fetchWeatherByCoords(lat, lon) {
  try {
    const url = `${CONFIG.BASE_URL}/weather?lat=${lat}&lon=${lon}&appid=${CONFIG.API_KEY}&units=metric`;
    const response = await fetch(url);
    if (!response.ok) throw new Error("Failed to fetch weather");
    const data = await response.json();
    displayWeather(data);
    input.value = data.name; // show city name in search
  } catch (error) {
    showError(error.message);
  } finally {
    hideLoading();
  }
}

Dynamic UI

Render weather data with icons, colors, and animations.

// Transform API response into display:
function displayWeather(data) {
  const weatherDisplay = document.querySelector(".weather-display");
  weatherDisplay.classList.remove("hidden");

  const { name, main, weather, wind, sys } = data;
  const icon = weather[0].icon;
  const iconUrl = `https://openweathermap.org/img/wn/${icon}@2x.png`;

  weatherDisplay.innerHTML = `
    <div class="weather-card">
      <div class="location">
        <h2>${escapeHtml(name)}, ${sys.country}</h2>
        <p class="date">${formatDate(new Date())}</p>
      </div>
      <div class="temperature">
        <img src="${iconUrl}" alt="${weather[0].description}">
        <span class="temp">${Math.round(main.temp)}°C</span>
      </div>
      <p class="description">${weather[0].description}</p>
      <div class="details">
        <div class="detail">
          <span class="label">Feels like</span>
          <span class="value">${Math.round(main.feels_like)}°C</span>
        </div>
        <div class="detail">
          <span class="label">Humidity</span>
          <span class="value">${main.humidity}%</span>
        </div>
        <div class="detail">
          <span class="label">Wind</span>
          <span class="value">${Math.round(wind.speed)} m/s</span>
        </div>
      </div>
    </div>
  `;

  // Dynamic background based on weather:
  updateBackground(weather[0].main);
}

function updateBackground(condition) {
  const app = document.querySelector(".app");
  const backgrounds = {
    Clear: "linear-gradient(135deg, #f6d365, #fda085)",
    Clouds: "linear-gradient(135deg, #bdc3c7, #2c3e50)",
    Rain: "linear-gradient(135deg, #667db6, #0082c8)",
    Snow: "linear-gradient(135deg, #e6dada, #274046)",
    Thunderstorm: "linear-gradient(135deg, #0f0c29, #302b63)",
  };
  app.style.background = backgrounds[condition] || backgrounds.Clear;
}

function formatDate(date) {
  return date.toLocaleDateString("en-US", {
    weekday: "long", month: "short", day: "numeric",
  });
}

Error Handling

Graceful failures — never show a broken UI.

// UI state helpers:
const loadingEl = document.querySelector(".loading");
const errorEl = document.querySelector(".error");
const weatherDisplay = document.querySelector(".weather-display");

function showLoading() {
  loadingEl.classList.remove("hidden");
  weatherDisplay.classList.add("hidden");
  errorEl.classList.add("hidden");
}

function hideLoading() {
  loadingEl.classList.add("hidden");
}

function showError(message) {
  errorEl.textContent = message;
  errorEl.classList.remove("hidden");
  weatherDisplay.classList.add("hidden");
}

function hideError() {
  errorEl.classList.add("hidden");
}

// Network resilience — retry with backoff:
async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (response.ok) return response;
      if (response.status >= 400 && response.status < 500) {
        throw new Error("Client error"); // don't retry 4xx
      }
    } catch (error) {
      if (i === retries - 1) throw error;
      // Exponential backoff: 1s, 2s, 4s
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
    }
  }
}

// Cache recent searches (avoid redundant API calls):
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getCachedWeather(city) {
  const key = city.toLowerCase();
  const cached = cache.get(key);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }
  const data = await fetchWeatherRaw(city);
  cache.set(key, { data, timestamp: Date.now() });
  return data;
}

// Key patterns in this project:
// ✓ Real API integration with error handling
// ✓ Loading/error/success UI states
// ✓ Geolocation API
// ✓ Caching to reduce API calls
// ✓ Retry with exponential backoff
// ✓ Dynamic UI based on data
// ✓ Input sanitization (encodeURIComponent, escapeHtml)

FAQ

Common questions about this project.