Build a Weather App with AI: Step-by-Step Project Guide
Your previous projects lived inside the browser. This one reaches out to the real world. You'll build a weather app that talks to an external API, fetches live data, and displays it beautifully — all with AI writing the code while you learn what every piece does.
TL;DR
You'll build a weather app that fetches live data from the OpenWeatherMap API. Type a city, get current temperature, conditions, humidity, and wind speed. This is your first project that makes real API calls — the skill that separates static pages from real web apps. Stack: HTML + CSS + vanilla JavaScript + fetch API. Cost: free. Time: ~1.5 hours.
Why Build This?
Your to-do app taught you DOM manipulation, event handling, and localStorage. Everything happened inside the browser. But real web apps don't just store data locally — they talk to servers on the internet to get and send information.
A weather app is the perfect introduction to this because:
- You'll learn the fetch API — the built-in JavaScript tool for making HTTP requests. Every web app uses this (or something like it) to get data from servers.
- You'll work with a real API — OpenWeatherMap gives you live weather data, and the free tier is generous enough for learning.
- You'll handle async code — your app needs to wait for data to come back from the internet. That means
async/await, which shows up in almost every modern JavaScript project. - You'll deal with real-world problems — network errors, API keys, rate limits, and data that doesn't look the way you expected. These are the exact things that break AI-generated code in the real world.
- You'll understand JSON — the format servers use to send structured data. When your AI generates code that parses API responses, you'll know what it's doing.
This is the project that takes you from "I can build things that look nice" to "I can build things that actually do something." It's the gateway to every app that relies on external data — from social media dashboards to e-commerce sites to the SaaS product you'll eventually want to ship.
What You'll Need
- An AI coding tool — Cursor, Windsurf, Claude Code, or even ChatGPT. Any tool that can generate code from a prompt works.
- A code editor — VS Code or Cursor (which is VS Code with AI built in).
- A free OpenWeatherMap API key — we'll walk through getting this in Step 3. It takes 2 minutes to sign up and the free tier gives you 1,000 calls per day.
- A web browser — Chrome or Firefox with DevTools. You'll use the console to debug API responses.
- Basic HTML/CSS/JS knowledge — if you've built the portfolio or to-do app projects, you're ready.
No framework needed. This project uses plain HTML, CSS, and JavaScript. No React, no Node.js, no build tools. Just files in a folder that you open in a browser.
Step 1: Tell Your AI What to Build
Here's the exact prompt you'll give your AI. This is specific enough to get good output on the first try, but leaves room for the AI to make design choices:
"Build a weather app in a single index.html file (inline CSS and JS). Requirements: (1) A search input where the user types a city name and presses Enter or clicks a Search button. (2) Display the current weather: city name, temperature in Fahrenheit, weather condition (like 'Cloudy' or 'Clear'), an icon from the API, humidity percentage, and wind speed in mph. (3) Use the OpenWeatherMap Current Weather API (api.openweathermap.org/data/2.5/weather). (4) Use async/await with fetch. (5) Show a loading state while fetching. (6) Show a clear error message if the city isn't found or the network fails. (7) Dark theme, centered card design, clean modern UI. (8) Put the API key in a variable at the top of the script so it's easy to find and replace."
Let's break down why each part of this prompt matters:
- "Single index.html file" — keeps it simple. No build step, no multiple files to manage. You can open it directly in a browser.
- "Inline CSS and JS" — everything in one place for learning. You can split it into separate files later.
- "async/await with fetch" — tells the AI to use modern syntax instead of older callback-based patterns. This is the standard way to make API calls in 2026.
- "Show a loading state" — AI often skips this. Without it, users click search and nothing happens for 1-2 seconds while the API responds.
- "Clear error message" — another thing AI often forgets. Without it, your app silently breaks when the city doesn't exist.
- "API key in a variable" — prevents the AI from burying the key inside a URL string where you can't find it.
Step 2: What AI Generated
Here's the complete code your AI will produce (cleaned up and annotated). This is a working weather app — once you add your API key, it fetches real weather data.
The Complete HTML/CSS/JS
<!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>
<style>
/* ===== BASE STYLES ===== */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #0A0E1A;
color: #E2E8F0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* ===== CARD CONTAINER ===== */
.weather-app {
background: #1A1F2E;
border-radius: 16px;
padding: 2rem;
width: 100%;
max-width: 420px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.weather-app h1 {
text-align: center;
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #60A5FA;
}
/* ===== SEARCH FORM ===== */
.search-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.search-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #2D3748;
border-radius: 8px;
background: #0A0E1A;
color: #E2E8F0;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
.search-form input:focus {
border-color: #60A5FA;
}
.search-form button {
padding: 0.75rem 1.25rem;
background: #3B82F6;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.search-form button:hover {
background: #2563EB;
}
/* ===== WEATHER DISPLAY ===== */
.weather-display {
text-align: center;
display: none; /* Hidden until data loads */
}
.weather-display.active {
display: block;
}
.city-name {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.weather-icon {
width: 100px;
height: 100px;
}
.temperature {
font-size: 3.5rem;
font-weight: 800;
color: #60A5FA;
}
.condition {
font-size: 1.1rem;
color: #94A3B8;
text-transform: capitalize;
margin-bottom: 1.5rem;
}
.details {
display: flex;
justify-content: space-around;
padding-top: 1.5rem;
border-top: 1px solid #2D3748;
}
.detail-item {
text-align: center;
}
.detail-label {
font-size: 0.8rem;
color: #64748B;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.detail-value {
font-size: 1.25rem;
font-weight: 600;
margin-top: 0.25rem;
}
/* ===== LOADING STATE ===== */
.loading {
text-align: center;
padding: 2rem;
color: #64748B;
display: none;
}
.loading.active {
display: block;
}
/* ===== ERROR STATE ===== */
.error {
text-align: center;
padding: 1.5rem;
color: #F87171;
background: rgba(248, 113, 113, 0.1);
border-radius: 8px;
display: none;
}
.error.active {
display: block;
}
</style>
</head>
<body>
<div class="weather-app">
<h1>🌤 Weather</h1>
<form class="search-form" id="search-form">
<input
type="text"
id="city-input"
placeholder="Enter city name..."
autocomplete="off"
required
>
<button type="submit">Search</button>
</form>
<div class="loading" id="loading">
Fetching weather data...
</div>
<div class="error" id="error"></div>
<div class="weather-display" id="weather-display">
<div class="city-name" id="city-name"></div>
<img class="weather-icon" id="weather-icon" alt="Weather icon">
<div class="temperature" id="temperature"></div>
<div class="condition" id="condition"></div>
<div class="details">
<div class="detail-item">
<div class="detail-label">Humidity</div>
<div class="detail-value" id="humidity"></div>
</div>
<div class="detail-item">
<div class="detail-label">Wind</div>
<div class="detail-value" id="wind"></div>
</div>
<div class="detail-item">
<div class="detail-label">Feels Like</div>
<div class="detail-value" id="feels-like"></div>
</div>
</div>
</div>
</div>
<script>
// ========================================
// YOUR API KEY — Replace this with yours
// ========================================
const API_KEY = 'YOUR_API_KEY_HERE'
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather'
// ===== DOM REFERENCES =====
const form = document.getElementById('search-form')
const input = document.getElementById('city-input')
const weatherDisplay = document.getElementById('weather-display')
const loadingEl = document.getElementById('loading')
const errorEl = document.getElementById('error')
// ===== FETCH WEATHER DATA =====
async function getWeather(city) {
// Build the URL with query parameters
const url = `${BASE_URL}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=imperial`
// Show loading, hide everything else
showLoading()
try {
// Make the API request
const response = await fetch(url)
// Check if the response was successful
if (!response.ok) {
if (response.status === 404) {
throw new Error(`City "${city}" not found. Check the spelling and try again.`)
}
if (response.status === 401) {
throw new Error('Invalid API key. Check your OpenWeatherMap API key.')
}
throw new Error('Something went wrong. Please try again.')
}
// Parse the JSON response
const data = await response.json()
// Display the weather data
displayWeather(data)
} catch (error) {
showError(error.message)
}
}
// ===== DISPLAY WEATHER =====
function displayWeather(data) {
// Hide loading and error
loadingEl.classList.remove('active')
errorEl.classList.remove('active')
// Populate the weather display
document.getElementById('city-name').textContent =
`${data.name}, ${data.sys.country}`
document.getElementById('temperature').textContent =
`${Math.round(data.main.temp)}°F`
document.getElementById('condition').textContent =
data.weather[0].description
document.getElementById('weather-icon').src =
`https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`
document.getElementById('humidity').textContent =
`${data.main.humidity}%`
document.getElementById('wind').textContent =
`${Math.round(data.wind.speed)} mph`
document.getElementById('feels-like').textContent =
`${Math.round(data.main.feels_like)}°F`
// Show the weather display
weatherDisplay.classList.add('active')
}
// ===== UI STATE HELPERS =====
function showLoading() {
loadingEl.classList.add('active')
weatherDisplay.classList.remove('active')
errorEl.classList.remove('active')
}
function showError(message) {
loadingEl.classList.remove('active')
weatherDisplay.classList.remove('active')
errorEl.textContent = message
errorEl.classList.add('active')
}
// ===== EVENT HANDLER =====
form.addEventListener('submit', (e) => {
e.preventDefault()
const city = input.value.trim()
if (city) {
getWeather(city)
}
})
</script>
</body>
</html>
That's the full app. Copy this code, replace YOUR_API_KEY_HERE with your actual OpenWeatherMap API key (we'll get that in the next step), and open the file in a browser. You'll have a working weather app.
Step 3: Getting Your API Key
The weather app needs to identify itself to OpenWeatherMap's server. That's what an API key does — it's like a membership card that says "I'm allowed to request data." Here's how to get yours:
- Go to openweathermap.org/api and click "Sign Up" (or "Sign In" if you already have an account).
- Create a free account. You just need an email. No credit card.
- Once logged in, go to "API keys" in your profile. You'll see a default key already generated for you.
- Copy that key. It looks like a long random string:
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 - Paste it into your code, replacing
YOUR_API_KEY_HERE.
⚠️ New API keys take up to 2 hours to activate. If you just created your account and the app shows a 401 error ("Invalid API key"), wait a couple hours and try again. This trips up almost everyone — it's not your code, it's OpenWeatherMap's activation delay.
⚠️ Don't share your API key publicly. If you push this code to GitHub, anyone can see your key and use your quota. For a learning project, this isn't a disaster (the free tier is limited anyway). For production apps, you'd use environment variables on a backend server. We'll cover that in the "What AI Gets Wrong" section below.
Step 4: Understanding the Code
Let's walk through every important piece. You don't need to memorize this — you need to understand it well enough to debug problems and tell your AI what to fix.
The API URL Structure
const url = `${BASE_URL}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=imperial`
// This builds a URL like:
// https://api.openweathermap.org/data/2.5/weather?q=Seattle&appid=abc123&units=imperial
This URL has four parts:
BASE_URL— the server address (where to send the request)?q=Seattle— the query parameter (what city you want)&appid=abc123— your API key (proving you're authorized)&units=imperial— tells the API to return Fahrenheit and mph (usemetricfor Celsius and km/h)
encodeURIComponent(city) handles city names with spaces or special characters. "New York" becomes "New%20York" so the URL doesn't break. Your AI usually includes this, but sometimes it doesn't — and the app works fine until someone searches for "San Francisco" and gets an error.
async/await — Waiting for the Internet
async function getWeather(city) {
const response = await fetch(url)
const data = await response.json()
}
This is the pattern you'll see in every app that talks to a server:
async— marks the function as asynchronous (it does something that takes time)await fetch(url)— sends the request and waits for the response to come back. Withoutawait, JavaScript would try to use the response before it arrives — and crash.await response.json()— the response comes as raw text..json()converts it into a JavaScript object you can work with. This also takes time (it's reading the response body), so it needsawaittoo.
If you've never seen async/await, check out What Is Async/Await? for the full explanation. The short version: it lets your code say "do this thing, wait for it to finish, then continue" — without freezing the entire page.
The API Response (What the Server Sends Back)
When you call the OpenWeatherMap API for Seattle, you get back a JSON object that looks like this:
{
"name": "Seattle",
"main": {
"temp": 52.3,
"feels_like": 49.1,
"humidity": 78
},
"weather": [
{
"main": "Clouds",
"description": "overcast clouds",
"icon": "04d"
}
],
"wind": {
"speed": 8.5
},
"sys": {
"country": "US"
}
}
Notice the nesting. Temperature isn't at data.temp — it's at data.main.temp. The weather description isn't at data.description — it's at data.weather[0].description (the [0] means "the first item in the array").
This is one of the most common sources of bugs in API projects. Your AI usually gets the paths right because it knows the OpenWeatherMap API format, but if something shows "undefined" on screen, this is the first thing to check.
💡 Pro debugging tip: Add console.log(data) right after const data = await response.json(). Open your browser's DevTools (F12 → Console tab) and you'll see the entire response object. Click the arrows to expand it and see every field. This is how you figure out the exact path to any piece of data.
Error Handling — The try/catch Block
try {
const response = await fetch(url)
if (!response.ok) {
if (response.status === 404) {
throw new Error(`City "${city}" not found.`)
}
if (response.status === 401) {
throw new Error('Invalid API key.')
}
throw new Error('Something went wrong.')
}
const data = await response.json()
displayWeather(data)
} catch (error) {
showError(error.message)
}
Here's what's happening:
try { ... }— "Try running this code. If anything goes wrong, jump to the catch block instead of crashing."response.ok— a boolean that'struefor successful responses (status 200-299) andfalsefor errors (400, 401, 404, 500, etc.).throw new Error()— manually creates an error with a custom message. This triggers thecatchblock.catch (error)— catches any error that happened in the try block — whether it's a network failure, an API error, or something you threw manually. Theerror.messageis the text you passed tonew Error().
Without try/catch, your app crashes silently when the network is down or the city doesn't exist. The user sees nothing. With it, you show a helpful error message. This is the pattern your AI should use every time it makes an API call — but often doesn't.
UI State Management (Loading, Error, Display)
function showLoading() {
loadingEl.classList.add('active')
weatherDisplay.classList.remove('active')
errorEl.classList.remove('active')
}
function showError(message) {
loadingEl.classList.remove('active')
weatherDisplay.classList.remove('active')
errorEl.textContent = message
errorEl.classList.add('active')
}
The app has three mutually exclusive states: loading, error, or showing weather data. These functions toggle CSS classes to show one and hide the others. The CSS uses display: none by default and display: block when the .active class is added. Simple, effective, and the same basic pattern used in every frontend framework — just without the framework.
Step 5: Common Issues and Fixes
Issue: "Invalid API key" (401 Error)
Cause: Your API key is new and hasn't been activated yet, or you pasted it incorrectly.
Fix: Wait 2 hours after creating the key. Double-check that you copied the entire key with no extra spaces. The key should be a string of letters and numbers — no quotes around it in the URL, but quotes around it in the JavaScript variable.
// ✅ Correct
const API_KEY = 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
// ❌ Wrong — extra space at the end
const API_KEY = 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 '
// ❌ Wrong — still has placeholder
const API_KEY = 'YOUR_API_KEY_HERE'
Issue: Temperature Shows "undefined"
Cause: Your code is accessing the wrong property path in the API response.
Fix: The temperature lives at data.main.temp, not data.temp or data.temperature. Add console.log(data) to see the actual response structure. Tell your AI: "The temperature data is at data.main.temp — update the display function."
Issue: "Failed to fetch" (Network Error)
Cause: No internet connection, the API server is down, or there's a CORS issue.
Fix: Check your internet connection first. Then open DevTools (F12 → Network tab) and look at the failed request. If you see a CORS error, it usually means you're calling the API from a file:// URL instead of a local server.
// ❌ If your browser's address bar shows:
// file:///C:/Users/you/weather-app/index.html
// Some browsers block API calls from file:// URLs
// ✅ Run a local server instead:
// Open terminal in your project folder and run:
npx serve .
// Then open http://localhost:3000
Issue: Weather Icon Doesn't Load
Cause: The icon URL is wrong or the icon code from the API wasn't used correctly.
Fix: OpenWeatherMap icons follow this pattern: https://openweathermap.org/img/wn/{icon_code}@2x.png. The icon code comes from data.weather[0].icon (like "04d" for overcast clouds). Make sure your code uses this path and not a different icon service.
Issue: Search Works Once, Then Breaks
Cause: The form is submitting and reloading the page.
Fix: Make sure you have e.preventDefault() in your form submit handler. This stops the browser from actually submitting the form (which would reload the page and lose your JavaScript state).
form.addEventListener('submit', (e) => {
e.preventDefault() // <-- This line is critical
const city = input.value.trim()
if (city) getWeather(city)
})
What AI Gets Wrong
AI coding tools will get you 90% of the way there with a weather app — but that last 10% is where real bugs live. Here's what your AI will likely miss or do poorly:
1. Hardcoded API Keys
Every AI tool will put the API key directly in the JavaScript code. For a learning project, this is fine. For anything you deploy publicly, it's a problem — anyone who views your page source can steal your key.
The real fix: Use a backend proxy. Your JavaScript calls your own server, and your server calls OpenWeatherMap with the key stored as an environment variable. Your AI can set this up, but it won't do it unless you ask.
"Create a simple Express.js backend proxy for my weather app. The backend should have one endpoint — GET /api/weather?city=Seattle — that calls the OpenWeatherMap API using an API key from process.env.WEATHER_API_KEY, then returns the data to the frontend. The frontend should call my backend instead of OpenWeatherMap directly."
2. No Error Handling
If you don't explicitly ask for error handling, many AI tools will generate a bare fetch call with no try/catch. The app works perfectly when you search for "Seattle" — and silently breaks when you search for "asdfgh" or lose internet.
Always tell your AI: "Add error handling for network failures, invalid city names, and API errors. Show user-friendly error messages."
3. No Loading State
API calls take 200ms to 2 seconds depending on the server and your connection. Without a loading indicator, the user clicks "Search" and nothing happens — they think the app is broken and click again (sending a duplicate request).
Always tell your AI: "Show a loading indicator while fetching data. Disable the search button during the request to prevent duplicate submissions."
4. No Input Validation
AI-generated code often doesn't check if the input is empty or contains only whitespace. Submitting an empty search sends a useless API request and returns confusing errors.
// ❌ What AI often generates
form.addEventListener('submit', (e) => {
e.preventDefault()
getWeather(input.value) // Empty string? No problem! (narrator: it was a problem)
})
// ✅ What it should generate
form.addEventListener('submit', (e) => {
e.preventDefault()
const city = input.value.trim()
if (!city) return // Don't search for nothing
getWeather(city)
})
5. Hardcoded Units
AI usually picks either Fahrenheit or Celsius and hardcodes it. Half your users will want the other one. A quick fix is adding a toggle button, but the AI won't include this unless you ask.
Level Up: Add These Features
Once your basic weather app is working, use these prompts to extend it. Each one teaches you a new concept:
1. Five-Day Forecast
"Add a 5-day forecast below the current weather. Use the OpenWeatherMap forecast endpoint (api.openweathermap.org/data/2.5/forecast). Show the day name, high/low temperature, and weather icon for each day. Display them in a horizontal scrollable row."
What you'll learn: Working with arrays of data, looping through API results, and rendering multiple items from a single response. The forecast API returns data in 3-hour intervals — you'll need to filter it to get one entry per day.
2. Geolocation (Auto-Detect City)
"Add a 'Use My Location' button that detects the user's city using the browser's Geolocation API (navigator.geolocation). Pass the latitude and longitude to the OpenWeatherMap API instead of a city name. Handle the case where the user denies location permission."
What you'll learn: Browser APIs beyond the DOM, user permissions, and using latitude/longitude coordinates instead of city names. The OpenWeatherMap API accepts lat and lon parameters.
3. Fahrenheit/Celsius Toggle
"Add a toggle button to switch between Fahrenheit and Celsius. When toggled, re-fetch the weather data with units=imperial or units=metric. Remember the user's preference in localStorage."
What you'll learn: Storing user preferences, re-fetching data with different parameters, and the localStorage pattern you learned in the to-do app.
4. Recent Searches History
"Add a recent searches feature. Save the last 5 cities searched to localStorage. Display them as clickable chips below the search bar. Clicking a chip searches for that city again. Add a 'Clear History' button."
What you'll learn: Combining localStorage with dynamic UI — similar to the to-do app but applied in a new context. You'll also practice array manipulation (limiting to 5 items, preventing duplicates).
5. Weather-Based Background
"Change the app's background gradient based on the current weather condition. Sunny = warm yellow/orange gradient, cloudy = gray gradient, rainy = dark blue gradient, snowy = light blue/white gradient. Use the weather condition code from the API to determine which gradient to show. Animate the transition when it changes."
What you'll learn: Conditional styling based on data, CSS gradients, and CSS transitions — turning raw data into visual design decisions.
What to Learn Next
This weather app introduced you to some of the most important concepts in web development. Here's where to go deeper:
Frequently Asked Questions
Is the OpenWeatherMap API free?
Yes. OpenWeatherMap offers a free tier that gives you 1,000 API calls per day and 60 calls per minute. That's more than enough for a personal weather app. You just need to sign up for a free account at openweathermap.org and generate an API key. The key can take up to 2 hours to activate after creation.
Why is my weather app showing "undefined" instead of temperature?
This usually means the API response structure doesn't match what your code expects. The OpenWeatherMap API nests temperature inside response.main.temp. If your code tries to access response.temp or response.temperature directly, you'll get undefined. Add console.log(data) after parsing the response to see its actual structure, then update your code to match.
What is the fetch API and why does the weather app use it?
The fetch API is a built-in JavaScript function that makes HTTP requests — it's how your browser asks another server for data. Your weather app uses fetch to send a request to the OpenWeatherMap server, which responds with weather data in JSON format. It's asynchronous (uses async/await), meaning the page doesn't freeze while waiting for the response. Learn more in What Is the Fetch API?
Can I build a weather app without a backend server?
Yes, for a personal project. The OpenWeatherMap API supports direct browser requests (CORS-enabled), so you can call it from plain HTML/JavaScript without a backend. However, this means your API key is visible in the source code. For a production app, you'd want a backend proxy to keep your key secret using environment variables. For learning and personal use, direct browser calls work fine.
What's the difference between this weather app and the to-do app project?
The to-do app taught you DOM manipulation, event handling, and localStorage — everything stays in the browser. The weather app introduces external API calls: your code talks to a server on the internet, waits for a response, and displays that live data. This is a fundamental shift — your app now depends on the outside world, which means new challenges like network errors, API keys, rate limits, and asynchronous code.