Ad – 728×90
🛠️ Projects

JavaScript Weather App – Build with Fetch API

Build a real weather application that searches cities by name and fetches live data from OpenWeatherMap's free API. Displays temperature, humidity, wind speed, and weather icons with robust error handling for invalid cities and network failures.

⏱️ 28 min read 🎯 Advanced 📅 Updated 2026

API Setup

This project uses the OpenWeatherMap API (free tier — 60 calls/minute, no credit card required).

  1. Go to openweathermap.org and create a free account.
  2. Navigate to API Keys in your account dashboard.
  3. Copy your API key (it activates within a few minutes).
  4. Replace YOUR_API_KEY in the code below with your actual key.
API Key Security: Never commit your API key to a public GitHub repository. For a deployed production app, proxy requests through a backend server. For a personal portfolio project, using the key in client-side JavaScript is acceptable.
API EndpointPurposeFree Tier
Current WeatherTemperature, humidity, wind, iconYes
5-Day Forecast3-hour interval forecastsYes
GeocodingCity name → coordinatesYes
One Call API 3.0Hourly + daily forecastsNo (paid)

HTML Structure

JavaScript
/* weather.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Weather App</title>
  <link rel="stylesheet" href="weather-style.css">
</head>
<body>
  <div class="weather-app">
    <div class="search-bar">
      <input id="city-input" type="text" placeholder="Enter city name…" autocomplete="off">
      <button id="search-btn">Search</button>
    </div>

    <div id="loading" class="loading hidden">Fetching weather…</div>
    <div id="error"   class="error   hidden"></div>

    <div id="weather-card" class="weather-card hidden">
      <div class="weather-top">
        <div>
          <h2 id="city-name"></h2>
          <p id="weather-desc"></p>
        </div>
        <img id="weather-icon" alt="Weather icon">
      </div>
      <div class="temp-main">
        <span id="temp"></span>
        <button id="unit-toggle">Switch to °F</button>
      </div>
      <div class="weather-details">
        <div class="detail"><span>💧 Humidity</span><span id="humidity"></span></div>
        <div class="detail"><span>💨 Wind</span><span id="wind"></span></div>
        <div class="detail"><span>🌡️ Feels Like</span><span id="feels-like"></span></div>
        <div class="detail"><span>👁️ Visibility</span><span id="visibility"></span></div>
      </div>
    </div>
  </div>
  <script src="weather-script.js"></script>
</body>
</html>
*/

API Service Layer

Separate the API call into its own function. This makes the code testable and easy to swap for a different API provider in the future.

JavaScript
// weather-script.js

const API_KEY  = "YOUR_API_KEY"; // Replace with your OpenWeatherMap key
const BASE_URL = "https://api.openweathermap.org/data/2.5/weather";

// Custom error types
class CityNotFoundError extends Error {
  constructor(city) {
    super(`City "${city}" not found. Check the spelling and try again.`);
    this.name = "CityNotFoundError";
  }
}
class NetworkError extends Error {
  constructor() {
    super("Network error. Check your internet connection and try again.");
    this.name = "NetworkError";
  }
}

// Fetch weather data from OpenWeatherMap
async function fetchWeather(city, units = "metric") {
  const url = `${BASE_URL}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=${units}`;

  let response;
  try {
    response = await fetch(url);
  } catch {
    throw new NetworkError();
  }

  if (response.status === 404) throw new CityNotFoundError(city);
  if (!response.ok) throw new Error(`API Error: ${response.status} ${response.statusText}`);

  return response.json();
}

UI Controller

JavaScript
// State
let currentUnit = "metric"; // "metric" (°C) | "imperial" (°F)
let lastCity    = "";

// DOM refs
const cityInput   = document.getElementById("city-input");
const searchBtn   = document.getElementById("search-btn");
const loadingEl   = document.getElementById("loading");
const errorEl     = document.getElementById("error");
const cardEl      = document.getElementById("weather-card");
const unitToggle  = document.getElementById("unit-toggle");

// UI state helpers
function showLoading() {
  loadingEl.classList.remove("hidden");
  cardEl.classList.add("hidden");
  errorEl.classList.add("hidden");
}
function showError(msg) {
  errorEl.textContent = msg;
  errorEl.classList.remove("hidden");
  loadingEl.classList.add("hidden");
}
function showCard() {
  cardEl.classList.remove("hidden");
  loadingEl.classList.add("hidden");
  errorEl.classList.add("hidden");
}

// Populate weather card
function displayWeather(data) {
  const unitSymbol = currentUnit === "metric" ? "°C" : "°F";
  const speedUnit  = currentUnit === "metric" ? "m/s" : "mph";

  document.getElementById("city-name").textContent =
    `${data.name}, ${data.sys.country}`;
  document.getElementById("weather-desc").textContent =
    data.weather[0].description;
  document.getElementById("weather-icon").src =
    `https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`;
  document.getElementById("weather-icon").alt =
    data.weather[0].description;

  document.getElementById("temp").textContent =
    `${Math.round(data.main.temp)}${unitSymbol}`;
  document.getElementById("humidity").textContent =
    `${data.main.humidity}%`;
  document.getElementById("wind").textContent =
    `${data.wind.speed} ${speedUnit}`;
  document.getElementById("feels-like").textContent =
    `${Math.round(data.main.feels_like)}${unitSymbol}`;
  document.getElementById("visibility").textContent =
    `${(data.visibility / 1000).toFixed(1)} km`;

  unitToggle.textContent = currentUnit === "metric"
    ? "Switch to °F" : "Switch to °C";

  showCard();
}

// Main search handler
async function handleSearch() {
  const city = cityInput.value.trim();
  if (!city) return;
  lastCity = city;
  showLoading();

  try {
    const data = await fetchWeather(city, currentUnit);
    displayWeather(data);
  } catch (err) {
    showError(err.message);
  }
}

// Unit toggle (re-fetch with new unit)
unitToggle.addEventListener("click", async () => {
  currentUnit = currentUnit === "metric" ? "imperial" : "metric";
  if (!lastCity) return;
  showLoading();
  try {
    const data = await fetchWeather(lastCity, currentUnit);
    displayWeather(data);
  } catch (err) {
    showError(err.message);
  }
});

searchBtn.addEventListener("click", handleSearch);
cityInput.addEventListener("keydown", e => {
  if (e.key === "Enter") handleSearch();
});

// Load a default city on startup
cityInput.value = "London";
handleSearch();
Sample API response (condensed):
{ name: "London", sys: { country: "GB" }, main: { temp: 14.2, humidity: 72, feels_like: 13.1 }, weather: [{ description: "light rain", icon: "10d" }], wind: { speed: 5.1 }, visibility: 10000 }
Concepts Practiced: Fetch API, async/await, custom error classes, encodeURIComponent for URL-safe city names, response.ok checking, loading state management, unit conversion, and DOM update patterns.

CSS Highlights

JavaScript
/* weather-style.css — key styles

.weather-app {
  max-width: 420px;
  margin: 40px auto;
  padding: 24px;
  background: linear-gradient(135deg, #1e3a5f, #2d6a9f);
  border-radius: 24px;
  color: white;
  font-family: 'Segoe UI', sans-serif;
  box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.search-bar { display: flex; gap: 10px; margin-bottom: 20px; }
.search-bar input {
  flex: 1;
  padding: 12px 16px;
  border-radius: 12px;
  border: none;
  font-size: 16px;
}
.weather-card { animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; } }
.temp-main { font-size: 64px; font-weight: 200; text-align: center; margin: 20px 0; }
.weather-details { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.detail {
  background: rgba(255,255,255,0.15);
  padding: 12px;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.hidden { display: none !important; }
.error { color: #fca5a5; text-align: center; padding: 20px; }
*/

Enhancements

Extend the Weather App

  • 5-day forecast — use the /forecast endpoint and display daily highs/lows
  • Geolocation — use navigator.geolocation to auto-detect the user's city on page load
  • Search history — store the last 5 searched cities in localStorage as quick-access buttons
  • Dynamic background — change the gradient based on weather condition (sunny = yellow, rain = grey, snow = light blue)
  • AbortController — cancel the previous fetch if the user searches a new city before the first resolves

AbortController Implementation

Implement request cancellation so rapid searches don't cause race conditions:

let abortController = null;

async function handleSearch() {
  // Cancel any in-flight request
  if (abortController) abortController.abort();
  abortController = new AbortController();

  const city = cityInput.value.trim();
  if (!city) return;
  showLoading();

  try {
    const url = `${BASE_URL}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=${currentUnit}`;
    const res = await fetch(url, { signal: abortController.signal });
    if (!res.ok) throw new Error(...);
    const data = await res.json();
    displayWeather(data);
  } catch (err) {
    if (err.name === "AbortError") return; // ignore cancelled requests
    showError(err.message);
  }
}

FAQ

Why does fetch not throw on 404 responses?

The Fetch API only rejects (throws) on network-level failures like no internet connection. A 404 is a valid HTTP response, so fetch resolves successfully. You must always check response.ok or response.status manually.

How do I handle cities with the same name (e.g., multiple "Springfield" cities)?

Add a country code parameter: q=Springfield,US. OpenWeatherMap also supports lat and lon parameters for precise location queries — useful with the Geocoding API.

Why use encodeURIComponent(city)?

City names with spaces, accents, or special characters (e.g., "São Paulo") would break the URL. encodeURIComponent percent-encodes them into a URL-safe format.

Can I build this without an API key using a different data source?

Yes — wttr.in provides weather data as JSON with no API key required: https://wttr.in/London?format=j1. It's less featureful but great for learning.

How do I deploy this to GitHub Pages?

Push your HTML/CSS/JS files to a GitHub repo, go to Settings → Pages, and select the main branch. Your app will be live at username.github.io/repo-name. Be aware the API key will be visible in client-side code.

Summary

  • API setup: get a free OpenWeatherMap key, use the Current Weather endpoint
  • Fetch pattern: always check response.ok before calling response.json()
  • Custom error classes: CityNotFoundError and NetworkError for typed handling
  • Loading states: show/hide elements based on current async status
  • Unit toggle: re-fetch with units=imperial parameter instead of converting client-side
  • This project demonstrates real-world async/await usage, not just toy examples
  • Proper fetch error handling (checking response.ok) is a very common interview question
  • Custom error classes show OOP knowledge applied to practical error handling
  • Loading state management is a pattern used in every production JavaScript application