Almost every app you'll ever build does the same core thing: it asks another computer for data and shows it to a person. A weather app is the smallest honest version of that. Type "Tokyo," get Tokyo's forecast. Simple to describe, and it teaches the exact skills — fetch, async/await, reading real API docs, handling failure — that every bigger project reuses.
This assumes you've built something small before, like the Calculator or Todo App from the project library — you can write a function, change the page from JavaScript, and read an error in the console. That's enough.
Estimated time: ~8 hours. The code is short. The time goes into the two things that are genuinely new: understanding why async code works the way it does, and learning to read an API's documentation to find what you need. Don't rush those — they're the whole point.
What you're building
A single page. An input for a city name, a search button, and a card that shows the current conditions once you search. No framework, no build step — just an index.html, a little CSS, and one JavaScript file. You'll be able to open it in a browser and, at the end, put it on the real internet with a URL you can share.
The data source: Open-Meteo, a free weather API that needs no sign-up and no API key. That keeps you unblocked and, importantly, keeps you out of a trap: an API key is a secret, and secrets don't belong in code that runs in a browser. Open-Meteo has nothing to hide, so it's perfect for a first client-side app.
Most APIs do require a key, and doing that safely means putting the key on a server the browser never sees — exactly what the AI Chatbot project teaches. For now, a keyless API lets you focus on one new thing at a time. That's a deliberate learning choice, not a shortcut.
Milestone 0 — The page and a plan
Create three files in a folder:
weather-app/
index.html
style.css
app.js
A minimal index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Weather</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main>
<h1>Weather</h1>
<form id="search">
<input id="city" placeholder="Enter a city…" />
<button type="submit">Search</button>
</form>
<div id="result"></div>
</main>
<script src="app.js"></script>
</body>
</html>
Open it in your browser. It does nothing yet — that's fine. Before writing any JavaScript, get the plan straight, because it explains every line that follows. To show weather for "Tokyo," you actually need two API calls:
- Geocoding — turn the name "Tokyo" into coordinates (latitude and longitude). Weather APIs speak in coordinates, not city names.
- Forecast — ask for the weather at those coordinates.
Understanding that upfront — that "search by city" is two steps, not one — is the difference between following a recipe and knowing what you're cooking.
Milestone 1 — Your first fetch
Before touching the page, prove you can talk to the API at all. In app.js:
async function getCoordinates(city) {
const url =
"https://geocoding-api.open-meteo.com/v1/search?name=" +
encodeURIComponent(city) +
"&count=1";
const response = await fetch(url);
const data = await response.json();
console.log(data);
return data;
}
getCoordinates("Tokyo");
Reload the page, open the browser console, and read what prints. This is the most important habit in the whole project: look at the real data before you write code that uses it. You'll see a results array with latitude, longitude, and name fields. You didn't guess that structure — you looked.
Now the part people memorize instead of understand. Slow down here:
fetch(url)doesn't return the data — it returns a promise. A network request takes time (tens or hundreds of milliseconds), and JavaScript refuses to freeze the whole page waiting. Sofetchimmediately hands you an IOU: "I'll have a response later."awaitis you saying "I'll wait for that IOU before continuing." The line pauses until the response arrives, then gives you the real value. It only works inside anasyncfunction — that keyword is what makesawaitlegal.- Why
.json()needs its ownawait: the response arrives as a raw stream of text; parsing it into a usable object is itself an async step. Two awaits, two waits — one for the response, one for reading its body.
The number-one beginner bug: forgetting await and trying to use the promise as if it were the data. const data = fetch(url) gives you a promise object, and data.results is undefined. If a value is mysteriously undefined or a "Promise" prints where you expected data, a missing await is the first thing to check. Understanding why — that the value isn't there yet — is worth more than the fix.
Read the Open-Meteo geocoding docs as you do this. Learning to find the one parameter you need in a page of documentation is a real skill, and it's the one being trained right now.
Milestone 2 — Get the forecast and show it
Now the second call. Once you have coordinates, ask for the weather:
async function getWeather(lat, lon) {
const url =
"https://api.open-meteo.com/v1/forecast?latitude=" +
lat +
"&longitude=" +
lon +
"¤t=temperature_2m,weather_code";
const response = await fetch(url);
const data = await response.json();
return data.current;
}
Again: log it, read the shape, then render it. Put the result on the page instead of the console:
function render(cityName, current) {
const result = document.getElementById("result");
result.innerHTML =
"<h2>" + cityName + "</h2>" +
"<p>" + current.temperature_2m + "°C</p>";
}
Chain the two calls together so a city name flows all the way to a rendered card:
async function showWeather(city) {
const geo = await getCoordinates(city);
const place = geo.results[0];
const current = await getWeather(place.latitude, place.longitude);
render(place.name, current);
}
showWeather("Tokyo");
Notice the shape: showWeather awaits the first call, uses its result to make the second, and awaits that. Async code reads top-to-bottom like normal code — that's the whole gift of await. Each line genuinely waits for the one before it, so you can reason about it in order.
Milestone 3 — Wire up the search box
Replace the hardcoded "Tokyo" with what the user types. Listen for the form's submit:
document.getElementById("search").addEventListener("submit", (event) => {
event.preventDefault();
const city = document.getElementById("city").value.trim();
if (city) showWeather(city);
});
event.preventDefault() is the line everyone forgets, and forgetting it is baffling until you understand it: a form's default behavior is to reload the page, which wipes your result the instant it appears. You're telling the browser "I'll handle this myself, don't reload." Knowing why the page flashed and cleared — not just that this line fixes it — is exactly the kind of understanding that compounds.
Search a few cities. You have a working weather app. Commit it now — this is the natural checkpoint to run Cartara against your repo (see the last section).
Milestone 4 — The unhappy paths (this is where it gets real)
Right now, type "asdfgh" and the app breaks silently — geo.results is undefined and the code throws. Real apps don't do that. Handle the three states every data-driven app has:
- Loading. A network call takes time; show something during it. Set the result area to "Loading…" before the
await, so the user knows it's working. - Not found. If
geo.resultsis missing or empty, the city didn't match. Show a friendly "Couldn't find that city" instead of crashing. - Error. The network can fail — offline, API down. Wrap the calls in
try/catchand show a real message.
async function showWeather(city) {
const result = document.getElementById("result");
result.textContent = "Loading…";
try {
const geo = await getCoordinates(city);
if (!geo.results || geo.results.length === 0) {
result.textContent = "Couldn't find that city. Try another spelling.";
return;
}
const place = geo.results[0];
const current = await getWeather(place.latitude, place.longitude);
render(place.name, current);
} catch (err) {
result.textContent = "Something went wrong. Check your connection and try again.";
}
}
This milestone is what separates a tutorial-follower from a developer. Anyone can render the happy path. Thinking through "what if the city doesn't exist? what if the network is down?" — and handling each on purpose — is the actual job. If an interviewer asks you to walk through this project, this is the part worth talking about.
Optional polish once the states work: map Open-Meteo's numeric weather_code to a word and an emoji ("Clear ☀️", "Rain 🌧️") by reading the code table in their docs. It's a small, satisfying exercise in — again — reading the documentation.
Milestone 5 — Put it on the internet
A project without a URL is invisible. Because this is just static files, deploying is nearly free:
- Push the folder to a GitHub repo.
- Turn on GitHub Pages (repo Settings → Pages → deploy from your main branch), or drag the folder onto Netlify. Either gives you a public URL in minutes.
- Open the live link on your phone and search your own city.
That's the loop that makes it real: it's not "code on my laptop," it's a thing on the internet that works for anyone.
Stuck? Study and remix
- Open-Meteo — the API's own repo and docs. When a parameter isn't doing what you expect, the answer is in there.
- public-apis/public-apis — a giant list of free APIs. The best way to prove you learned this rather than copied it: rebuild the same app against a different API (currency rates, a dictionary, dog photos). If you can, the skill is yours.
Build it with Cartara
You could get this working by pasting each block and never quite grasping why await matters or why the page kept reloading. It would run — and it would fall apart the moment you tried to change it, or an interviewer asked "what does await actually do here?" That gap between "it works" and "I understand it" is invisible from the outside, and it's the exact thing that gets bigger when an AI assistant writes the code for you.
That gap is what Cartara exists to close. As you build:
- Point Cartara at your repo. It watches the code that lands — including anything an AI helper generated — and turns each real change into a moment to check your understanding, not just a commit.
- Explain your changes back. When you add the two-call flow or the error handling, Cartara asks you the why — the same question a reviewer or interviewer would.
- See your comprehension, not just your output. You get an honest signal on which parts of your own first app you truly understand — so you can shore up the weak spots before you move on to something harder.
That's the whole idea behind this library: build something real, then prove to yourself you understand it. The weather app is the project. The understanding is the point.
Make your first real app one you actually understand
Point Cartara at your weather-app repo and it captures what you genuinely learned — the honest way to turn a first project into a skill you own.