This is the project where you connect to the outside world. The interesting part is not the weather — it is handling the three things that can happen when you ask another server for data: it works, it is still working, or it failed.
The HTML#
<main class="app">
<h1>Weather</h1>
<form id="search">
<label class="sr" for="city">City</label>
<input id="city" type="text" placeholder="Lahore" required autocomplete="off">
<button type="submit">Check</button>
</form>
<div id="status" role="status" aria-live="polite"></div>
<section id="result" hidden>
<h2 id="place"></h2>
<p id="temp" class="temp"></p>
<p id="desc"></p>
<dl id="detail"></dl>
</section>
</main>
<script src="weather.js" defer></script>Two details that matter more than they look:
role="status"witharia-live="polite"means a screen reader announces “Loading” and any error without the user having to go looking for it.- The result section starts
hiddenand we toggle.hiddenin JavaScript, rather than fighting withstyle.display.
Step 1: the three states#
Write these before writing any fetch code. If the states are right, the rest is straightforward.
const form = document.querySelector("#search");
const input = document.querySelector("#city");
const statusEl = document.querySelector("#status");
const result = document.querySelector("#result");
function showLoading(city) {
statusEl.textContent = `Looking up ${city}…`;
result.hidden = true;
}
function showError(message) {
statusEl.textContent = message;
result.hidden = true;
}
function showWeather(data) {
statusEl.textContent = "";
result.hidden = false;
document.querySelector("#place").textContent = data.place;
document.querySelector("#temp").textContent = `${Math.round(data.temperature)}°`;
document.querySelector("#desc").textContent = data.description;
document.querySelector("#detail").innerHTML = `
<dt>Feels like</dt><dd>${Math.round(data.feelsLike)}°</dd>
<dt>Humidity</dt><dd>${data.humidity}%</dd>
<dt>Wind</dt><dd>${data.wind} km/h</dd>
`;
}Notice showWeather reads data.place, data.temperature and so on — names we chose. Nothing here knows which provider the data came from.
Step 2: the adapter#
This is the only function that changes if you switch providers. Open your API’s documentation, make one real request in the browser address bar, look at what comes back, and map it:
// Fill these in from YOUR provider's documented response.
// Make one real request first and look at the actual JSON.
function normalise(raw) {
return {
place: raw.name,
temperature: raw.main.temp,
feelsLike: raw.main.feels_like,
humidity: raw.main.humidity,
wind: raw.wind.speed,
description: raw.weather[0].description,
};
}Step 3: fetching#
const API_BASE = "https://api.example.com/weather";
const API_KEY = "your-key-here";
async function getWeather(city) {
const url = `${API_BASE}?q=${encodeURIComponent(city)}&units=metric&appid=${API_KEY}`;
const response = await fetch(url);
if (response.status === 404) {
throw new Error(`No city called "${city}" was found.`);
}
if (response.status === 401) {
throw new Error("The API key was rejected.");
}
if (!response.ok) {
throw new Error(`The weather service returned ${response.status}.`);
}
return normalise(await response.json());
}Three things worth pointing at:
fetchdoes not throw on a 404 or a 500. It only rejects when the request could not be made at all. You must checkresponse.okor a specific status yourself — see the Fetch API guide.encodeURIComponenthandles cities with spaces and accents. Without it, “New York” breaks the URL.- Turning statuses into messages here keeps the user-facing wording in one place, and each one says what the person can do about it.
Step 4: wiring it up#
let inFlight = null;
form.addEventListener("submit", async (event) => {
event.preventDefault();
const city = input.value.trim();
if (!city) return;
// Cancel an earlier request so a slow answer cannot overwrite a newer one.
if (inFlight) inFlight.abort();
inFlight = new AbortController();
showLoading(city);
try {
const data = await getWeather(city, inFlight.signal);
showWeather(data);
} catch (err) {
if (err.name === "AbortError") return; // superseded, not a failure
showError(err.message || "Something went wrong. Please try again.");
console.error(err);
}
});To make the abort work, pass the signal through:
async function getWeather(city, signal) {
const response = await fetch(url, { signal });
// ...
}Without this, someone who searches twice quickly can see the first (slower) result land on top of the second. It is a real bug in a lot of tutorial weather apps.
The CSS#
.app {
max-width: 420px;
margin: 48px auto;
padding: 24px;
border-radius: 16px;
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, .07);
font-family: system-ui, sans-serif;
}
#search { display: flex; gap: 8px; margin-bottom: 20px; }
#city { flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px; font: inherit; }
button { padding: 10px 18px; border: 0; border-radius: 8px; background: #4f46e5; color: #fff; font: inherit; cursor: pointer; }
#status { min-height: 1.4em; color: #666; font-size: .9rem; }
.temp { font-size: 3rem; margin: 4px 0; font-weight: 700; }
#detail { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin-top: 16px; font-size: .9rem; }
#detail dt { color: #777; }
#detail dd { margin: 0; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }#status has a min-height so the layout does not jump when the message appears and disappears.
Questions people ask#
Which weather API should I use?
Several offer a free tier, and some need no key at all. Pick one whose documentation you find readable — you will be looking at it a lot. Check whether it allows requests directly from a browser, because some do not, which produces a CORS error you cannot fix from the front end.
I get a CORS error
The provider has not permitted browser requests from other origins. You cannot work around this in your JavaScript. Either pick an API that allows it, or make the call from your own server.
Why is my temperature showing as undefined?
Your normalise() is reading a field that does not exist in the response. Log the raw JSON and compare it against the names you are using.
Should I use axios instead of fetch?
Not for this. fetch is built in and does everything here. Axios adds conveniences you will appreciate later on larger projects.