API Bouncer

Buy me a coffee

Building a Weather Dashboard: Step-by-Step Tutorial

Let's build something real. In this tutorial, you'll create a weather dashboard that shows current conditions and a 5-day forecast for any city in the world. We'll use the Open-Meteo API, which is completely free and requires no API key. By the end, you'll have a functional app you can deploy anywhere.

What we're building

The finished dashboard will have:

The HTML structure

Create an index.html file. We'll keep the markup clean and semantic:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Weather Dashboard</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <h1>Weather Dashboard</h1> <div class="search-bar"> <input type="text" id="city-input" placeholder="Enter city name..."> <button id="search-btn">Search</button> <button id="location-btn">Use My Location</button> </div> <div id="error-msg" class="error" hidden></div> <div id="current-weather" class="card" hidden> <h2 id="city-name"></h2> <div class="current-details"> <span id="temperature" class="temp"></span> <div class="meta"> <p>Wind: <span id="wind"></span></p> <p>Condition: <span id="condition"></span></p> </div> </div> </div> <div id="forecast" class="forecast-grid" hidden></div> </div> <script src="app.js"></script> </body> </html>

The CSS

Create style.css. We'll use a simple, responsive layout:

* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f4f8; color: #333; padding: 2rem; } .container { max-width: 700px; margin: 0 auto; } h1 { text-align: center; margin-bottom: 1.5rem; } .search-bar { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; } #city-input { flex: 1; padding: 0.75rem; border: 2px solid #ddd; border-radius: 8px; font-size: 1rem; min-width: 200px; } button { padding: 0.75rem 1.25rem; border: none; border-radius: 8px; cursor: pointer; font-size: 0.95rem; background: #3b82f6; color: #fff; } button:hover { background: #2563eb; } .card { background: #fff; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08); } .current-details { display: flex; align-items: center; gap: 2rem; margin-top: 1rem; } .temp { font-size: 3rem; font-weight: 700; } .forecast-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1rem; } .forecast-grid .card { text-align: center; padding: 1rem; } .forecast-grid .card p { margin-top: 0.3rem; } .error { color: #dc2626; background: #fef2f2; padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }

The JavaScript

Create app.js. This is where the real work happens. Let's build it function by function.

Weather code mapping

Open-Meteo returns numeric weather codes. We need to translate them to human-readable descriptions:

const weatherCodes = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast', 45: 'Foggy', 48: 'Icy fog', 51: 'Light drizzle', 53: 'Drizzle', 55: 'Heavy drizzle', 61: 'Light rain', 63: 'Rain', 65: 'Heavy rain', 71: 'Light snow', 73: 'Snow', 75: 'Heavy snow', 80: 'Rain showers', 81: 'Heavy rain showers', 85: 'Snow showers', 95: 'Thunderstorm', };

Geocoding: turning city names into coordinates

Open-Meteo needs latitude and longitude. Their geocoding API converts city names:

async function geocodeCity(name) { const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1&language=en`; const res = await fetch(url); if (!res.ok) throw new Error('Geocoding request failed'); const data = await res.json(); if (!data.results?.length) throw new Error(`City "${name}" not found`); const { latitude, longitude, name: cityName, country } = data.results[0]; return { latitude, longitude, displayName: `${cityName}, ${country}` }; }

Fetching weather data

async function getWeather(lat, lon) { const params = new URLSearchParams({ latitude: lat, longitude: lon, current_weather: true, daily: 'temperature_2m_max,temperature_2m_min,weathercode', timezone: 'auto', forecast_days: 5 }); const res = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`); if (!res.ok) throw new Error('Weather request failed'); return res.json(); }

Rendering the UI

function renderCurrent(name, weather) { const cw = weather.current_weather; document.getElementById('city-name').textContent = name; document.getElementById('temperature').textContent = `${Math.round(cw.temperature)}°C`; document.getElementById('wind').textContent = `${cw.windspeed} km/h`; document.getElementById('condition').textContent = weatherCodes[cw.weathercode] || 'Unknown'; document.getElementById('current-weather').hidden = false; } function renderForecast(weather) { const container = document.getElementById('forecast'); const { time, temperature_2m_max, temperature_2m_min, weathercode } = weather.daily; container.innerHTML = time.map((date, i) => ` <div class="card"> <strong>${new Date(date).toLocaleDateString('en', { weekday: 'short', month: 'short', day: 'numeric' })}</strong> <p>${Math.round(temperature_2m_max[i])}° / ${Math.round(temperature_2m_min[i])}°</p> <p>${weatherCodes[weathercode[i]] || ''}</p> </div> `).join(''); container.hidden = false; }

Wiring up the event handlers

function showError(msg) { const el = document.getElementById('error-msg'); el.textContent = msg; el.hidden = false; } function clearError() { document.getElementById('error-msg').hidden = true; } async function searchCity(name) { clearError(); try { const geo = await geocodeCity(name); const weather = await getWeather(geo.latitude, geo.longitude); renderCurrent(geo.displayName, weather); renderForecast(weather); } catch (err) { showError(err.message); } } document.getElementById('search-btn').addEventListener('click', () => { const city = document.getElementById('city-input').value.trim(); if (city) searchCity(city); }); document.getElementById('city-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { const city = e.target.value.trim(); if (city) searchCity(city); } }); document.getElementById('location-btn').addEventListener('click', () => { clearError(); if (!navigator.geolocation) { return showError('Geolocation is not supported by your browser.'); } navigator.geolocation.getCurrentPosition( async (pos) => { try { const weather = await getWeather(pos.coords.latitude, pos.coords.longitude); renderCurrent('Your Location', weather); renderForecast(weather); } catch (err) { showError(err.message); } }, () => showError('Unable to get your location. Please allow location access or search manually.') ); });

How it all fits together

When the user types a city and clicks Search, we geocode the city name to coordinates, fetch the weather data for those coordinates, and render both the current conditions and the 5-day forecast. The geolocation button skips the geocoding step and feeds the browser's coordinates directly into the weather fetch.

Error handling is layered: the geocoding function throws if no city is found, the weather function throws on network failure, and both are caught in searchCity() which displays the error to the user.

Deploying it

Since this is a static site (three files, no build step), you can deploy it anywhere:

No server needed. No environment variables. No API keys to protect. This is one of the beauties of using a keyless API like Open-Meteo.

Going further

You could extend this project by adding hourly temperature charts (using Chart.js), saving favorite cities to localStorage, showing weather icons, or adding unit toggling between Celsius and Fahrenheit. Each of those is a great exercise in working with API data. For more weather-related APIs, check out our Weather API category.