API Setup
This project uses the OpenWeatherMap API (free tier — 60 calls/minute, no credit card required).
- Go to openweathermap.org and create a free account.
- Navigate to API Keys in your account dashboard.
- Copy your API key (it activates within a few minutes).
- Replace
YOUR_API_KEYin the code below with your actual key.
| API Endpoint | Purpose | Free Tier |
|---|---|---|
| Current Weather | Temperature, humidity, wind, icon | Yes |
| 5-Day Forecast | 3-hour interval forecasts | Yes |
| Geocoding | City name → coordinates | Yes |
| One Call API 3.0 | Hourly + daily forecasts | No (paid) |
HTML Structure
/* 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.
// 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
// 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();
{ 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 }
CSS Highlights
/* 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
/forecastendpoint and display daily highs/lows - Geolocation — use
navigator.geolocationto 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.okbefore callingresponse.json() - Custom error classes:
CityNotFoundErrorandNetworkErrorfor typed handling - Loading states: show/hide elements based on current async status
- Unit toggle: re-fetch with
units=imperialparameter 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