Build Project

Learn by Building

Home › Build Projects › Web Apps › Weather App

Web App · Beginner

Build a Weather App

Vanilla JavaScript, real API calls, proper loading and error states. No API key required.

⏱️ 60 minutes · 📄 3 files · 🎯 Beginner

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.

🎬 Live Preview

The three files

weather-app/
├── index.html      ← Structure
├── style.css       ← Appearance
└── app.js          ← API calls & DOM updates
1

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>
2

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; }
3

app.js — the API calls

Two API calls in sequence: first geocoding (city name → coordinates), then weather (coordinates → temperature).

app.js
const 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.

1. Show loading state first

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.

2. await the response, then await the JSON

Two steps: await fetch() gives you a Response object. await response.json() parses the body. Both must be awaited.

3. Handle the "no data" case explicitly

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.

4. Wrap everything in try/catch

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

undefined is not iterable

You destructured geoData.results[0] without checking geoData.results exists. Always guard before accessing properties from API responses.

CORS error

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.

The result never updates

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.

City not found for a real city

Some names are ambiguous. "Springfield" matches dozens of cities. Add the country code: "Springfield, US".

🎯 What to add next

❓ 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.

🔗 Related

← All Build Projects