Build a Weather Dashboard with Node.js
Build a full-stack weather app with Express.js and a real weather API.
Introduction
30 Second Summary
Every time you check the weather on your phone, an app is sending a request to a server, which talks to a weather service and sends data back to your screen. That chain of communication is how most modern web apps work.
In this project, you will build a weather dashboard from scratch that follows this exact pattern. You'll create a server, connect it to a real weather API, and build a frontend that displays live weather data for any city in the world.
What You'll Build
A weather dashboard web app where users type a city name and see current weather conditions, powered by a backend Express.js server you write yourself.
By the end of this project, you'll have:
- An Express.js backend server that handles API routes and serves your frontend files.
- A frontend with HTML, CSS, and JavaScript that fetches data from your server and displays it dynamically.
- A working frontend-to-backend-to-API connection using the free Open-Meteo weather API. This is the same architecture pattern used in production web applications.
- Secret Mission: Add a 5-day forecast display with daily high and low temperatures.
Are there any prerequisites?
You should have basic programming knowledge (variables, functions, loops). No prior experience with Node.js or web servers is required.
Set Up the Project
Before you can build the weather dashboard, you need a project folder with the right tools installed.
In this step, you'll create your project from scratch using npm (the Node.js package manager), install Express.js as your server framework, and set up the file structure for the entire app.
In this step, get ready to:
- Create your project folder and initialize it with npm.
- Install Express.js as your web server framework.
- Create the file structure for your frontend and backend.
Create your project folder and initialize it
Every Node.js project starts with a folder and a package.json file that tracks your project's dependencies.
- In Cursor, click Terminal in the top menu bar.
- Click New Terminal.
A terminal panel opens at the bottom of Cursor.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir weather-dashboard
cd weather-dashboard
- Confirm you're inside the new folder by running:
pwd
You should see a path ending in /Desktop/weather-dashboard.
- Initialize your project by running:
npm init -y
- Confirm the file was created by running:
ls
You should see package.json in the output.
What is package.json?
package.json is a manifest file that tracks your project's name, version, and dependencies (the external packages your project uses).
The -y flag tells npm to accept all default values so you skip the interactive setup questions.
Not seeing package.json?
Make sure you ran npm init -y from inside the weather-dashboard folder. Run pwd to confirm your current directory ends with /weather-dashboard.
Help me troubleshoot my npm init command.
Install Express.js
Express.js is a web server framework for Node.js. It lets you create a server that responds to HTTP requests, like when a browser asks for a web page or data.
- Install Express.js by running:
npm install express@5.2.1
- Confirm Express was installed by running:
ls
You should now see node_modules and package-lock.json alongside your package.json.
What is node_modules?
The node_modules folder contains all the actual code for Express.js and its own dependencies. You never edit files in here directly.
package-lock.json records the exact versions installed so other developers get identical packages.
Install command failing?
Check that you have an internet connection. The npm install command downloads packages from the npm registry, so it needs network access.
Also confirm you're inside the weather-dashboard folder where package.json lives. Without that file in the current directory, npm cannot track the dependency.
Help me debug my Express install.
Create the file structure
Your project needs a backend file for the server and a public folder for all the frontend files that users see in their browser.
- Create the public folder by running:
mkdir public
Now you need to create four empty files. You'll write code in these files in the following steps.
- Open the weather-dashboard folder in Cursor by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the weather-dashboard folder, and click Open.
You should see the Explorer sidebar on the left showing node_modules, public, package-lock.json, and package.json.
- In the Explorer sidebar, hover over the WEATHER-DASHBOARD heading and click the new file icon (a page with a small + sign).
- Type server.js and press Enter.
- Click the public folder in the sidebar to expand it.
- Hover over the public folder name and click the new file icon.
- Type index.html and press Enter.
- Repeat for style.css inside public. Hover over public, click the new file icon, type style.css, and press Enter.
- Repeat one more time for app.js inside public. Hover over public, click the new file icon, type app.js, and press Enter.
- Verify all files exist by running this in your terminal:
ls
You should see node_modules, package-lock.json, package.json, public, and server.js in the output.
- Check the public folder by running:
ls public
You should see app.js, index.html, and style.css.
Why separate public and server files?
server.js runs on your machine (the backend). The files inside public/ get sent to the user's browser (the frontend).
Keeping them separate makes it clear which code runs where. Express will serve everything in public/ automatically to anyone who visits your site.
Files not showing in the Explorer sidebar?
Make sure you opened the weather-dashboard folder itself, not the Desktop. The sidebar title should say WEATHER-DASHBOARD at the top.
If you accidentally created files at the wrong level, delete them and try again. Right-click the file in the sidebar and select Delete.
Help me fix my project file structure.
Your project skeleton is ready. Next up, you'll write the Express server that powers the whole weather dashboard.
Build the Express Server
Your project folder is set up with Express installed and an empty file structure ready to go. Now it's time to write the actual server that powers your weather dashboard.
A web server is a program that listens for requests (like a browser asking for a page) and sends back responses. Your server will do two things: serve your frontend files and fetch weather data from an external API.
In this step, get ready to:
- Write an Express server that serves static files and handles API requests.
- Create a route that fetches real weather data from the Open-Meteo API.
- Start your server and verify it returns live weather data.
Write the server scaffold
Before adding any weather logic, you need a basic server that can start up and serve your frontend files. This is the foundation everything else builds on.
- Open server.js in Cursor and paste the following code:
// Import Express and create the app
const express = require('express');
const app = express();
const port = 3000;
// Serve files from the public/ folder (HTML, CSS, JS)
app.use(express.static('public'));
// Start the server
app.listen(port, () => {
console.log(`Weather dashboard server running at http://localhost:${port}`);
});
What does this code do?
- require('express') imports the Express framework you installed in Step 1.
- express() creates your app, which is the object you attach routes and settings to.
- app.use(express.static('public')) tells Express to automatically serve any file inside the public/ folder when a browser requests it.
- app.listen(port) starts the server on port 3000, ready to receive requests.
You won't see a change yet. The next chunk adds the weather API route, then you will run and test the full server.
Add the weather API route
Your server needs a route that accepts a city name, looks up its coordinates, and fetches the current weather. This route is the bridge between your frontend and the Open-Meteo API.
- In server.js, find this line:
// Start the server
app.listen(port, () => {
- Paste the following route handler directly above that // Start the server comment:
app.get('/api/weather', async (req, res) => {
// Read the city name from the URL query string
const city = req.query.city;
if (!city) {
return res.status(400).json({ error: 'City name is required' });
}
try {
// Convert city name to coordinates using Open-Meteo geocoding
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en`
);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
return res.status(404).json({ error: 'City not found' });
}
const { latitude, longitude, name, country } = geoData.results[0];
// Fetch current weather using the coordinates
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,relative_humidity_2m,weather_code`
);
const weatherData = await weatherResponse.json();
// Send the combined result back to the frontend
res.json({
city: name,
country: country,
temperature: weatherData.current.temperature_2m,
windSpeed: weatherData.current.wind_speed_10m,
humidity: weatherData.current.relative_humidity_2m,
weatherCode: weatherData.current.weather_code
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch weather data' });
}
});
What does this route do?
This route responds when your frontend visits /api/weather?city=London. It does two things in sequence:
- Geocoding: Calls Open-Meteo's geocoding API to convert the city name into latitude and longitude coordinates.
- Weather fetch: Uses those coordinates to call Open-Meteo's forecast API for current temperature, wind speed, humidity, and a weather code.
- async/await lets your server wait for each API response before moving to the next step. Without it, the code would try to use data that hasn't arrived yet.
Why does the server call the API instead of the frontend?
Your frontend could call Open-Meteo directly. But having the server make the call is the standard pattern used in professional apps.
It keeps API logic in one place, lets you add API keys later without exposing them to users, and gives you a single point to add caching or rate limiting.
- Save server.js.
You won't see a change yet. The next substep starts the server and tests it.
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js before moving on.
ⓧ I'd like to double check the full code
Here is the full server.js file. Make sure yours matches exactly:
const express = require('express');
const app = express();
const port = 3000;
app.use(express.static('public'));
app.get('/api/weather', async (req, res) => {
const city = req.query.city;
if (!city) {
return res.status(400).json({ error: 'City name is required' });
}
try {
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en`
);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
return res.status(404).json({ error: 'City not found' });
}
const { latitude, longitude, name, country } = geoData.results[0];
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,relative_humidity_2m,weather_code`
);
const weatherData = await weatherResponse.json();
res.json({
city: name,
country: country,
temperature: weatherData.current.temperature_2m,
windSpeed: weatherData.current.wind_speed_10m,
humidity: weatherData.current.relative_humidity_2m,
weatherCode: weatherData.current.weather_code
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch weather data' });
}
});
app.listen(port, () => {
console.log(`Weather dashboard server running at http://localhost:${port}`);
});
Start and test your server
Your server code is written. Time to start it up and confirm it actually works by hitting both the static file server and the weather API route.
- Start your server by running this command in the terminal:
node server.js
You should see this message in your terminal:
Weather dashboard server running at http://localhost:3000
What should I see?
If you see the message above, your server is running and listening for requests on port 3000. The terminal will stay occupied. This is normal. The server keeps running until you stop it.
- Open your browser and visit http://localhost:3000.
- You should see a blank white page. This is your empty index.html being served by Express.
- Now test the weather API route by visiting this URL in your browser:
http://localhost:3000/api/weather?city=London
- Copy that URL and paste it into your browser's address bar, then press Enter.
You should see raw JSON data in your browser with temperature, wind speed, humidity, and weather code values for London.
Not seeing JSON data?
- If you see an error about the module not being found, make sure you ran npm install express@5.2.1 in Step 1.
- If the page doesn't load at all, check that your terminal still shows the "running" message. If it shows an error, look for typos in your code.
- If you see {"error":"City name is required"}, make sure you included ?city=London at the end of the URL.
Help me debug my Express server
Understand the data flow
Before moving on, take a moment to understand how data flows through your app. This three-step pattern is the same architecture used in professional web applications.
How does the data flow work?
When you visit /api/weather?city=London, here is exactly what happens:
- Your server reads the city name from the URL query string (req.query.city).
- It calls https://geocoding-api.open-meteo.com/v1/search?name=London to convert "London" into latitude and longitude.
- It calls https://api.open-meteo.com/v1/forecast with those coordinates to get current temperature, wind speed, humidity, and weather code.
- It packages up the results and sends them back to the browser as JSON.
This is the Frontend -> Your Server -> External API pattern. Your frontend will never talk to Open-Meteo directly. It only talks to your server.
Your server is running and returning real weather data. Next up, you will build the frontend that calls this API and displays the results in a clean interface.
Build the Frontend
Your server is running and returning real weather data as JSON. Right now, though, the only way to see that data is by typing a URL directly into your browser.
In this step, you'll build the user-facing side of the app. You'll create an HTML page with a search form, style it with CSS, and write JavaScript that talks to your server and displays the results.
In this step, get ready to:
- Write the HTML structure with a search form and weather display card.
- Style the page with CSS for a clean, centered layout.
- Wire up JavaScript to fetch weather data from your server and display it on the page.
Write the HTML structure
Every web page starts with HTML that defines its structure. Your page needs a search form for the user to type a city name, and a card area where weather results will appear.
- In Cursor, open public/index.html from the Explorer sidebar.
- Paste the following HTML into the file:
<!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>
<!-- Search form: user types a city and clicks Search -->
<form id="search-form">
<input type="text" id="city-input" placeholder="Enter a city name..." required>
<button type="submit" id="search-button">Search</button>
</form>
<!-- Error message area: hidden by default -->
<div id="error-message" class="error hidden"></div>
<!-- Weather result card: hidden until a search succeeds -->
<div id="weather-result" class="weather-card hidden">
<h2 id="city-name"></h2>
<p id="weather-description" class="description"></p>
<div class="weather-details">
<div class="detail">
<span class="label">Temperature</span>
<span id="temperature" class="value"></span>
</div>
<div class="detail">
<span class="label">Wind Speed</span>
<span id="wind-speed" class="value"></span>
</div>
<div class="detail">
<span class="label">Humidity</span>
<span id="humidity" class="value"></span>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
What does this HTML do?
The page has three main sections that work together:
- A <form> with an input field and a submit button. The required attribute prevents empty submissions.
- An error div and a weather result card, both starting with a hidden class so they're invisible until JavaScript shows them.
- A <script> tag at the bottom that loads app.js after the HTML is parsed.
- Save public/index.html.
- Refresh your browser at http://localhost:3000. You should see the heading "Weather Dashboard", an input field, and a Search button. The weather card is invisible because it has the hidden class.
Seeing a blank page?
- Make sure your server is still running. If you stopped it, run node server.js in your terminal again.
- Check that index.html is saved inside the public/ folder, not in the project root.
Why is my page blank even though I saved index.html?
Style the page with CSS
The HTML structure is in place, but it looks plain. CSS will turn it into a clean, centered card layout with styled inputs and a weather result area.
- Open public/style.css in Cursor and paste the following styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f4f8;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* White card centered on the page */
.container {
background: white;
border-radius: 12px;
padding: 40px;
max-width: 480px;
width: 100%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 24px;
color: #1a202c;
}
/* Search form: input and button side by side */
#search-form {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
#city-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
#city-input:focus {
border-color: #4299e1;
}
#search-button {
padding: 12px 24px;
background-color: #4299e1;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
#search-button:hover {
background-color: #3182ce;
}
#search-button:disabled {
background-color: #a0aec0;
cursor: not-allowed;
}
/* Weather result card */
.weather-card {
background: #ebf8ff;
border-radius: 8px;
padding: 24px;
}
.weather-card h2 {
color: #2b6cb0;
margin-bottom: 4px;
}
.description {
color: #4a5568;
margin-bottom: 16px;
font-size: 18px;
}
.weather-details {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail {
display: flex;
justify-content: space-between;
align-items: center;
}
.label {
color: #718096;
font-size: 14px;
}
.value {
color: #1a202c;
font-weight: 600;
font-size: 18px;
}
/* Error message styling */
.error {
background: #fed7d7;
color: #c53030;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
}
/* Utility class to hide elements */
.hidden {
display: none;
}
What do these styles do?
- The body uses flexbox to center the .container both vertically and horizontally on the page.
- The .hidden class sets display: none. Your JavaScript will add and remove this class to show and hide elements.
- The #search-button:disabled style grays out the button while a search is in progress, giving the user visual feedback.
- Save public/style.css and refresh your browser.
- You should see a white card centered on a light gray background, with a text input and a blue Search button.
Page still looks unstyled?
- Check that the <link> tag in your HTML says href="style.css" (not styles.css).
- Make sure both index.html and style.css are in the same public/ folder.
Why aren't my CSS styles showing up in the browser?
Wire up the frontend JavaScript
The page looks good, but clicking Search does nothing. You need JavaScript that listens for form submissions, sends the city name to your server, and updates the page with the response.
- Open public/app.js in Cursor.
- Start by selecting all the DOM elements your code will interact with. Paste this at the top of the file:
// Select all the HTML elements we need to read from or write to
const searchForm = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchButton = document.querySelector('#search-button');
const weatherResult = document.querySelector('#weather-result');
const errorMessage = document.querySelector('#error-message');
const cityName = document.querySelector('#city-name');
const weatherDescription = document.querySelector('#weather-description');
const temperature = document.querySelector('#temperature');
const windSpeed = document.querySelector('#wind-speed');
const humidity = document.querySelector('#humidity');
What does this code do?
Each document.querySelector() call finds one HTML element by its id attribute and stores a reference to it in a variable. This means you can update those elements later without searching the page again every time.
- Now add the event listener that runs when the user submits the form. Paste this below the element selections:
searchForm.addEventListener('submit', async (event) => {
// Stop the form from refreshing the page
event.preventDefault();
const city = cityInput.value.trim();
if (!city) return;
// Show loading state
searchButton.disabled = true;
searchButton.textContent = 'Searching...';
weatherResult.classList.add('hidden');
errorMessage.classList.add('hidden');
try {
// Call YOUR server's API route
const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Something went wrong');
}
// Update the weather card with real data
cityName.textContent = `${data.city}, ${data.country}`;
temperature.textContent = `${data.temperature} °C`;
windSpeed.textContent = `${data.windSpeed} km/h`;
humidity.textContent = `${data.humidity}%`;
weatherResult.classList.remove('hidden');
} catch (error) {
// Show the error message to the user
errorMessage.textContent = error.message;
errorMessage.classList.remove('hidden');
} finally {
// Reset button state whether search succeeded or failed
searchButton.disabled = false;
searchButton.textContent = 'Search';
}
});
What's happening here?
- event.preventDefault() stops the browser from refreshing the page when the form submits. Without this, the user would lose the results instantly.
- fetch('/api/weather?city=...') sends a request to YOUR Express server (not directly to Open-Meteo). Your server handles the external API call.
- The finally block resets the button text regardless of success or failure. This ensures the user is never stuck with a grayed-out button.
Why call your own server instead of Open-Meteo directly?
Your frontend calls /api/weather on your server, which then calls Open-Meteo. This pattern (Frontend -> Your Server -> External API) keeps your backend in control of the data flow. If you ever need to add API keys, rate limiting, or caching, you handle it in one place on the server.
- Save public/app.js.
- Refresh your browser at http://localhost:3000.
- Type London in the search field and click Search. You should see a blue weather card appear with the city name, temperature, wind speed, and humidity.
- Type a nonsense word like xyzabc and click Search. You should see a red error message that says "City not found".
Search button does nothing?
- Open your browser's DevTools console (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for red error messages.
- Check that the id values in your HTML (like search-form, city-input) match the selectors in app.js exactly.
- Make sure the <script src="app.js"> tag is at the bottom of your HTML, just before </body>.
My search button is not fetching weather data. Help me debug.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for all three frontend files:
<!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>
<form id="search-form">
<input type="text" id="city-input" placeholder="Enter a city name..." required>
<button type="submit" id="search-button">Search</button>
</form>
<div id="error-message" class="error hidden"></div>
<div id="weather-result" class="weather-card hidden">
<h2 id="city-name"></h2>
<p id="weather-description" class="description"></p>
<div class="weather-details">
<div class="detail">
<span class="label">Temperature</span>
<span id="temperature" class="value"></span>
</div>
<div class="detail">
<span class="label">Wind Speed</span>
<span id="wind-speed" class="value"></span>
</div>
<div class="detail">
<span class="label">Humidity</span>
<span id="humidity" class="value"></span>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f4f8;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
max-width: 480px;
width: 100%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 24px;
color: #1a202c;
}
#search-form {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
#city-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
#city-input:focus {
border-color: #4299e1;
}
#search-button {
padding: 12px 24px;
background-color: #4299e1;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
#search-button:hover {
background-color: #3182ce;
}
#search-button:disabled {
background-color: #a0aec0;
cursor: not-allowed;
}
.weather-card {
background: #ebf8ff;
border-radius: 8px;
padding: 24px;
}
.weather-card h2 {
color: #2b6cb0;
margin-bottom: 4px;
}
.description {
color: #4a5568;
margin-bottom: 16px;
font-size: 18px;
}
.weather-details {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail {
display: flex;
justify-content: space-between;
align-items: center;
}
.label {
color: #718096;
font-size: 14px;
}
.value {
color: #1a202c;
font-weight: 600;
font-size: 18px;
}
.error {
background: #fed7d7;
color: #c53030;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.hidden {
display: none;
}
const searchForm = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchButton = document.querySelector('#search-button');
const weatherResult = document.querySelector('#weather-result');
const errorMessage = document.querySelector('#error-message');
const cityName = document.querySelector('#city-name');
const weatherDescription = document.querySelector('#weather-description');
const temperature = document.querySelector('#temperature');
const windSpeed = document.querySelector('#wind-speed');
const humidity = document.querySelector('#humidity');
searchForm.addEventListener('submit', async (event) => {
event.preventDefault();
const city = cityInput.value.trim();
if (!city) return;
searchButton.disabled = true;
searchButton.textContent = 'Searching...';
weatherResult.classList.add('hidden');
errorMessage.classList.add('hidden');
try {
const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Something went wrong');
}
cityName.textContent = `${data.city}, ${data.country}`;
temperature.textContent = `${data.temperature} °C`;
windSpeed.textContent = `${data.windSpeed} km/h`;
humidity.textContent = `${data.humidity}%`;
weatherResult.classList.remove('hidden');
} catch (error) {
errorMessage.textContent = error.message;
errorMessage.classList.remove('hidden');
} finally {
searchButton.disabled = false;
searchButton.textContent = 'Search';
}
});
Your weather dashboard is functional. You can search for any city in the world and see real-time weather data. Next up, you'll add human-readable weather descriptions and polish the user experience with loading states.
Add Weather Descriptions and Polish
Your weather app displays temperature, wind speed, and humidity. But the Open-Meteo API also sends back a weather code. This code is a number (like 0 for clear sky, 61 for rain, or 95 for thunderstorm). Showing a raw number to users is not helpful.
In this step, you'll write a function that translates these numeric codes into friendly descriptions like "Rainy" or "Clear sky". You'll connect it to the weather card so users see a real description alongside their data.
In this step, get ready to:
- Map WMO weather codes to human-readable descriptions.
- Display the weather description in the result card.
- Test the complete application with multiple cities.
Create the weather code mapping function
Open-Meteo uses the WMO (World Meteorological Organization) standard for weather codes. Each number maps to a specific condition. Your job is to write a function that takes a code and returns a friendly string.
- In public/app.js, find the line const humidity = document.querySelector('#humidity'); at the end of your DOM selectors.
- Add the following function below that line, before the searchForm.addEventListener block:
function getWeatherDescription(code) {
// Convert WMO weather codes to friendly text
if (code === 0) return 'Clear sky';
if (code <= 3) return 'Partly cloudy';
if (code <= 48) return 'Foggy';
if (code <= 57) return 'Drizzle';
if (code <= 67) return 'Rainy';
if (code <= 77) return 'Snowing';
if (code <= 82) return 'Rain showers';
if (code <= 86) return 'Snow showers';
if (code >= 95) return 'Thunderstorm';
return 'Unknown';
}
What does this code do?
This function takes a numeric weather code and returns a human-readable string. The logic uses a series of range checks that cascade from specific to general.
- code === 0 checks for the exact value 0, which means clear sky.
- code <= 3 catches codes 1, 2, and 3, which all mean partly cloudy or overcast.
- Each subsequent check catches a range. For example, codes 61 through 67 all represent rain of varying intensity.
- return 'Unknown' at the bottom is a fallback for any code that doesn't match the ranges above.
- Save app.js.
You won't see a change in your browser yet. The function exists but nothing calls it. The next substep connects it to the display.
Connect the description to the weather display
Your HTML already has a #weather-description paragraph element waiting to display text. Now you need to populate it with the result of your new function.
- In public/app.js, scroll down to the try block inside your event listener.
- Find this line:
cityName.textContent = `${data.city}, ${data.country}`;
- Add this line directly below it:
weatherDescription.textContent = getWeatherDescription(data.weatherCode);
What does this line do?
This calls your getWeatherDescription function with the numeric weatherCode from the API response. The returned string (like "Rainy" or "Clear sky") is placed into the description paragraph above the weather details.
- Save app.js.
Restart and test the complete application
Time to see everything working together. You'll restart the server and test the app with different cities to confirm the full flow.
- Switch back to your terminal in Cursor.
- Press Ctrl+C to stop the running server.
- Restart the server by running:
node server.js
- Open your browser to http://localhost:3000.
- Type London in the search field and click Search.
You should see the weather card appear with the city name, a weather description like "Partly cloudy" or "Rainy", plus temperature, wind speed, and humidity.
- Notice the button briefly shows "Searching..." while the data loads. This is the loading state your code already handles.
- Try searching for Tokyo and then Sydney to see different weather descriptions.
- Type a nonsense word like asdfqwerty and click Search to confirm the error message appears.
Weather description not showing up?
- Make sure you placed the getWeatherDescription function BETWEEN the DOM selectors and the addEventListener call. If it's inside the event listener, it won't be accessible.
- Check that the weatherDescription.textContent line is inside the try block, after the cityName.textContent line.
- Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) to check for any JavaScript errors.
Help me debug why my weather description isn't appearing on the page.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.js and your weather descriptions are showing correctly.
ⓧ I'd like to double check the full code
Here's the complete public/app.js file. Compare yours against it:
const searchForm = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchButton = document.querySelector('#search-button');
const weatherResult = document.querySelector('#weather-result');
const errorMessage = document.querySelector('#error-message');
const cityName = document.querySelector('#city-name');
const weatherDescription = document.querySelector('#weather-description');
const temperature = document.querySelector('#temperature');
const windSpeed = document.querySelector('#wind-speed');
const humidity = document.querySelector('#humidity');
function getWeatherDescription(code) {
if (code === 0) return 'Clear sky';
if (code <= 3) return 'Partly cloudy';
if (code <= 48) return 'Foggy';
if (code <= 57) return 'Drizzle';
if (code <= 67) return 'Rainy';
if (code <= 77) return 'Snowing';
if (code <= 82) return 'Rain showers';
if (code <= 86) return 'Snow showers';
if (code >= 95) return 'Thunderstorm';
return 'Unknown';
}
searchForm.addEventListener('submit', async (event) => {
event.preventDefault();
const city = cityInput.value.trim();
if (!city) return;
searchButton.disabled = true;
searchButton.textContent = 'Searching...';
weatherResult.classList.add('hidden');
errorMessage.classList.add('hidden');
try {
const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Something went wrong');
}
cityName.textContent = `${data.city}, ${data.country}`;
weatherDescription.textContent = getWeatherDescription(data.weatherCode);
temperature.textContent = `${data.temperature} °C`;
windSpeed.textContent = `${data.windSpeed} km/h`;
humidity.textContent = `${data.humidity}%`;
weatherResult.classList.remove('hidden');
} catch (error) {
errorMessage.textContent = error.message;
errorMessage.classList.remove('hidden');
} finally {
searchButton.disabled = false;
searchButton.textContent = 'Search';
}
});
Secret mission
Add a 5-Day Forecast
Your dashboard shows the current weather, but what about tomorrow? In this mission, you'll extend the app with a 5-day forecast display. This means modifying the backend to request daily data, then building a row of forecast cards on the frontend.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The Node.js server process running on port 3000.
- The weather-dashboard project folder on your Desktop (contains server.js, public/ folder, package.json, and node_modules/).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with the weather dashboard.
- Your server keeps running at http://localhost:3000 until you close the terminal or press Ctrl+C.
- Your project files stay on your Desktop in the weather-dashboard folder.
Pause - I'll come back to this later
Shut down the running server to free up port 3000. Your project files stay on your Desktop so you can restart anytime.
- In the terminal where your server is running, press Ctrl+C to stop the server.
- To restart later, open the weather-dashboard folder in Cursor and run node server.js in the terminal.
Delete - I don't want to use this again
Remove all project files and stop the server. This frees up disk space and port 3000.
- In the terminal where your server is running, press Ctrl+C to stop the server.
- Delete the project folder by running the command for your operating system.
macOS
cd ~/Desktop
rm -rf weather-dashboard
Windows
cd ~\Desktop
rmdir /s /q weather-dashboard
That's a Wrap!
That's a Wrap!
Nice work! You just built a full-stack weather dashboard from scratch, writing every line of code yourself.
You've learned how to:
- Created an Express.js web server that serves static files and handles API routes.
- Called an external API (Open-Meteo) from your server using fetch() and async/await.
- Built a frontend that communicates with your backend using the Fetch API, parses JSON data, and dynamically updates the DOM to display weather information.
- Secret Mission: Extended the app with a 5-day forecast display using array data and a loop.
Ready to quiz yourself?