Web App · Beginner
Build a Weather App
Vanilla JavaScript, real API calls, proper loading and error states. No API key required.
What you'll build: A weather app where you type a city and see the current temperature, weather condition, and wind speed. Real data from a real API. Loading states, error handling, all of it.
What you'll learn: fetch(), async/await, working with JSON responses, handling loading and error states, and calling multiple endpoints in sequence. This is the pattern behind every API-powered app.
The three files
weather-app/
├── index.html ← Structure
├── style.css ← Appearance
└── app.js ← API calls & DOM updates
index.html
Three elements: an input for the city, a search button, and a container for the result.
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<h1>🌤️ Weather</h1>
<div class="search">
<input type="text" id="cityInput" placeholder="Enter a city..." />
<button id="searchBtn">Search</button>
</div>
<div id="result"></div>
</div>
<script src="app.js"></script>
</body>
</html>
style.css
Same dark-mode aesthetic as the to-do app. Consistent across projects.
style.css* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 3rem 1rem;
}
.app {
width: 100%;
max-width: 500px;
background: #1e293b;
padding: 1.5rem;
border-radius: 1rem;
}
h1 {
font-size: 1.35rem;
margin-bottom: 1.25rem;
}
.search {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
input {
flex: 1;
background: #0f172a;
border: 1px solid #334155;
color: #e2e8f0;
padding: 0.65rem 0.85rem;
border-radius: 0.5rem;
font-size: 0.9rem;
font-family: inherit;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
button {
background: #3b82f6;
color: #fff;
border: none;
padding: 0 1rem;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
font-family: inherit;
}
button:hover { background: #2563eb; }
.result-card {
background: #0f172a;
padding: 1.25rem;
border-radius: 0.75rem;
}
.city-name {
font-size: 1.1rem;
font-weight: 700;
color: #fff;
margin-bottom: 0.5rem;
}
.temp {
font-size: 2rem;
font-weight: 800;
color: #3b82f6;
}
.detail {
color: #94a3b8;
font-size: 0.85rem;
margin-top: 0.4rem;
}
.loading { color: #3b82f6; font-size: 0.9rem; text-align: center; padding: 1rem; }
.error { color: #ef4444; font-size: 0.9rem; text-align: center; padding: 1rem; }
app.js — the API calls
Two API calls in sequence: first geocoding (city name → coordinates), then weather (coordinates → temperature).
app.jsconst input = document.getElementById('cityInput');
const searchBtn = document.getElementById('searchBtn');
const result = document.getElementById('result');
async function getWeather(city) {
// 1. Show loading state
result.innerHTML = '<div class="loading">⏳ Loading...</div>';
try {
// 2. Geocode the city name → coordinates
const geoRes = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geoData = await geoRes.json();
// 3. Handle the "no results" case
if (!geoData.results || geoData.results.length === 0) {
result.innerHTML = '<div class="error">❌ City not found. Try adding a country: "Karachi, PK".</div>';
return;
}
const { latitude, longitude, name, country } = geoData.results[0];
// 4. Fetch the weather for those coordinates
const weatherRes = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,weather_code,wind_speed_10m`
);
const weather = await weatherRes.json();
const current = weather.current;
// 5. Render the result
result.innerHTML = `
<div class="result-card">
<div class="city-name">${name}, ${country}</div>
<div class="temp">${current.temperature_2m}°C</div>
<div class="detail">💨 Wind: ${current.wind_speed_10m} km/h</div>
</div>
`;
} catch (err) {
// 6. Handle network errors
result.innerHTML = '<div class="error">❌ Network error. Check your connection.</div>';
console.error(err);
}
}
// Wire up events
searchBtn.addEventListener('click', () => {
const city = input.value.trim();
if (city) getWeather(city);
});
input.addEventListener('keydown', e => {
if (e.key === 'Enter') {
const city = input.value.trim();
if (city) getWeather(city);
}
});
// Initial demo
getWeather('London');
How the API pattern works
Every API call in a browser follows the same shape. Learn this pattern once and you'll recognize it everywhere.
Users need immediate feedback. As soon as the fetch starts, replace the UI with a "Loading..." message. Never leave them staring at a blank screen.
Two steps: await fetch() gives you a Response object. await response.json() parses the body. Both must be awaited.
A successful HTTP response doesn't mean the data is there. Check data.results.length before using it. This is why real apps have empty states.
Network errors are normal. Users go offline, APIs go down. try/catch lets you show a real error instead of a blank screen.
⚠️ Common problems
You destructured geoData.results[0] without checking geoData.results exists. Always guard before accessing properties from API responses.
The API you're calling doesn't allow browser requests. Open-Meteo does, which is why this tutorial uses it. If you switch APIs, check their CORS policy first.
You forgot to await one of the fetch() calls, or you're updating the DOM outside the async function. Both fetches must complete before you can render.
Some names are ambiguous. "Springfield" matches dozens of cities. Add the country code: "Springfield, US".
🎯 What to add next
- 5-day forecast. Add
dailyto the API parameters and render a small list. - Weather icons. Map
weather_codevalues to emoji or SVG icons. - Geolocation. Use
navigator.geolocationto auto-detect the user's city on load. - Search history. Save recent searches to localStorage and show them as chips.
- Unit toggle. Celsius ↔ Fahrenheit. The API supports both (
&temperature_unit=fahrenheit). - Dark/light mode. Swap colors based on the current time of day.
❓ Frequently asked questions
What will I learn building a weather app?
You'll learn how to call a real API, work with async/await, handle loading and error states, and render data that arrives asynchronously. These are the skills that separate toy projects from real apps.
Do I need an API key?
No. This tutorial uses Open-Meteo which is free and requires no API key. That's why it's perfect for a first API project.
How long does this build take?
About 60 minutes end-to-end. Longer if you follow each explanation carefully. The API concepts (fetch, async/await, error handling) are the parts worth slowing down for.
Why does the app show 'City not found' for a valid city?
The geocoding API matches on partial names. Try adding the country: 'Springfield, US' instead of just 'Springfield'. Some names are ambiguous — with over 30 cities called Springfield, you need to be specific.
Can I deploy this publicly?
Yes. Open-Meteo allows public use. Drag the folder to Netlify Drop or push to GitHub Pages. The app runs entirely in the browser.