AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Full Stack Capstone: Building a Complete Indian Weather App

📚 Web Development Foundations⏱️ 24 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 24 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open any weather app on your phone right now — the one that tells you it is 34°C in Delhi or that Chennai should expect rain this evening. Somewhere, a phone or laptop screen is drawing boxes, numbers and icons (that's one piece of software). Somewhere else, entirely different software is running weather models on satellite and sensor data, and packaging the results as a small, precise message (that's a second piece of software, usually on a machine you never see). And connecting them is a third thing: a strict, agreed-upon format for the request "give me Chennai's weather" and the reply "29.6°C, 78% humidity." None of these three pieces is "the app" by itself. The app is the fact that all three work together, correctly, every single time you tap the refresh button.

That three-piece system — a visible layer you interact with, an invisible layer that supplies real data, and the connecting logic between them — is what the phrase full stack refers to. In this chapter you are going to build all three pieces yourself, for real, and end up with a working app that shows live weather for five Indian cities. Every line of code in this chapter runs exactly as shown — you can paste it into a single .html file, open it in a browser, and it will fetch real weather data over the internet.

What "Full Stack" Actually Means

Before writing any code, get the vocabulary precise, because students often walk away from this topic with a vague and slightly wrong idea of what "full stack" means.

A full-stack application has three layers:

  • Frontend — everything that runs inside the user's browser: the HTML structure, the CSS appearance, and the JavaScript behaviour. This is the only layer the user can see or inspect directly (right-click → "View Page Source" on any website and you're looking at someone's frontend).
  • Backend — a program running on a server somewhere else in the world, which receives requests, does some work (calculations, database lookups, running a weather-prediction model), and sends back a result. You never see its code; you only see what it chooses to send you.
  • Data layer — the actual stored information the backend reads from and writes to: a database of user accounts, a database of weather station readings, a database of train timetables.

Here is the misconception worth correcting immediately: many students assume "full stack" always means "I personally wrote frontend code AND backend server code." That is one valid way to build a full-stack app, but it is not the only way. In this capstone, we are going to write the frontend layer ourselves, and we are going to consume a backend and data layer that someone else already built and made publicly available — a free weather API. Our app is still genuinely full-stack, because it still depends on all three layers working together; we have simply chosen to build one layer and reuse the other two. Professional developers do this constantly — nobody building a food delivery app writes their own maps engine or their own SMS-sending service from scratch. Knowing when to build a layer yourself and when to correctly plug into someone else's is itself a core full-stack skill.

Meeting the Data Layer: A Live Weather API

The backend we will connect to is called Open-Meteo, a free weather-data service that requires no signup and no secret password (called an "API key") to use for basic requests — which makes it ideal for learning, because you can test it immediately without registration steps getting in the way.

Before looking at how a computer receives this data, look at it the way you already understand data: as a table.

FieldValue for Chennai, right now
Temperature29.6
Humidity78
Wind speed11.2

That table has a name and a value in each row — exactly like a two-column mark sheet with "Subject" and "Marks." When a computer sends this same information over the internet, it doesn't send a drawn table; it sends text in a very particular format called JSON (JavaScript Object Notation). The table above, as JSON, looks like this:

{
  "temperature_2m": 29.6,
  "relative_humidity_2m": 78,
  "wind_speed_10m": 11.2
}

Read this exactly like the table: each line is "field name": value, separated by commas, with curly braces { } marking "this is one complete record." That's the entire idea of JSON — a labelled table, written as text, that both a JavaScript program and a human can read. The full response Open-Meteo actually sends nests this record inside a slightly larger structure, because it also tells you which city coordinates it answered for and what time the reading is from:

{
  "latitude": 13.0,
  "longitude": 80.25,
  "timezone": "Asia/Kolkata",
  "current": {
    "time": "2026-08-13T14:00",
    "temperature_2m": 29.6,
    "relative_humidity_2m": 78,
    "wind_speed_10m": 11.2
  },
  "current_units": {
    "temperature_2m": "°C",
    "relative_humidity_2m": "%",
    "wind_speed_10m": "km/h"
  }
}

Notice current is itself a JSON object — a table nested inside another table's row, the same way a spreadsheet cell can contain "see Sheet 2." To reach the temperature from JavaScript, you will write data.current.temperature_2m — read the dots left to right as "go into data, then into its current field, then read temperature_2m." (The exact numbers you receive when you run this yourself will differ from the ones above, since it is live weather — that's expected and correct; only the shape of the JSON stays fixed.)

To ask Open-Meteo for this data, your program sends a specially formatted web address (a URL) — no typing forms, no clicking buttons on their site, just a URL your code builds and requests directly:

https://api.open-meteo.com/v1/forecast?latitude=13.0827&longitude=80.2707&current=temperature_2m,relative_humidity_2m,wind_speed_10m&timezone=auto

The part after the ? is a set of instructions joined by &: which coordinates to look up, which fields you want in the reply, and to auto-detect the correct timezone. India's coordinates are fixed, well-documented numbers — Chennai sits at 13.0827° N, 80.2707° E; Delhi at 28.6139° N, 77.2090° E; Mumbai at 19.0760° N, 72.8777° E; Bengaluru at 12.9716° N, 77.5946° E; Kolkata at 22.5726° N, 88.3639° E. (India's own official weather agency, the India Meteorological Department, has been issuing forecasts since 1875 — one of the oldest such institutions in the world; Open-Meteo's global model is a different, independent data source we're using here purely because it's free and open for a student project.)

Layer 1: The Frontend Skeleton (HTML)

Start with structure only — no styling, no behaviour, just the pieces that need to exist on the page:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Bharat Weather</title>
</head>
<body>
  <div class="card">
    <h1>Bharat Weather</h1>
    <select id="citySelect">
      <option value="28.6139,77.2090">Delhi</option>
      <option value="19.0760,72.8777">Mumbai</option>
      <option value="12.9716,77.5946">Bengaluru</option>
      <option value="22.5726,88.3639">Kolkata</option>
      <option value="13.0827,80.2707">Chennai</option>
    </select>
    <button id="getWeatherBtn">Get Weather</button>
    <div id="result"></div>
  </div>
</body>
</html>

Three things deserve a second look. First, each <option value="..."> packs both coordinates into one string separated by a comma — a small trick that saves us from writing five separate if statements later; we'll split that string apart in the JavaScript. Second, <div id="result"></div> is deliberately empty — it's a container that JavaScript will fill in after the API replies; nothing appears there until code puts something there. Third, notice the id attributes (citySelect, getWeatherBtn, result) — these are the exact names our JavaScript will use to find these elements, so a typo here silently breaks everything downstream. This is a genuinely common bug: JavaScript that looks perfectly correct will fail with no visible error if it's searching for an id that doesn't match the HTML.

Layer 2: Making It Look Like an App (CSS)

Add this inside a <style> block in the <head>:

<style>
  body {
    font-family: Arial, sans-serif;
    background: #eef3f8;
    margin: 0;
    padding: 2rem;
  }
  .card {
    max-width: 420px;
    margin: 0 auto;
    background: #ffffff;
    border-radius: 12px;
    padding: 1.5rem;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  }
  select, button {
    font-size: 1rem;
    padding: 0.5rem;
    margin-top: 0.5rem;
    width: 100%;
    box-sizing: border-box;
  }
  button {
    background: #1a5fb4;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
  }
  #result {
    margin-top: 1.5rem;
    text-align: center;
    min-height: 80px;
  }
  #temp {
    font-size: 3rem;
    font-weight: bold;
    color: #1a5fb4;
  }
  .error {
    color: #c00000;
  }
</style>

Two rules here are doing work beyond plain decoration. box-sizing: border-box forces the browser to include padding inside the element's declared width, so a select set to width: 100% doesn't quietly overflow its card — a real, common layout bug that appears the moment you add padding to something already set to a fixed or percentage width. And min-height: 80px on #result stops the card from visibly "jumping" shorter while the div is empty and then snapping taller once weather data fills it in.

Layer 3: The Logic That Connects Everything (JavaScript)

This is the layer that actually makes the app "full stack" rather than just a static page — it's the code that talks to Open-Meteo's servers and threads the reply back into the HTML you already built.

Understanding the Wait: What "Asynchronous" Means

Before the code, understand one idea, because it trips up almost everyone the first time: when your program asks Open-Meteo's server for weather, the answer does not come back instantly. It has to travel across the internet, get processed, and travel back — this could take anywhere from 50 milliseconds to a few seconds depending on network conditions. JavaScript in a browser is single-threaded, meaning it can only actively execute one instruction at a time. If "waiting for the network" meant "freeze everything until the reply arrives," your entire browser tab would lock up — you couldn't scroll, click, or even see the page repaint — every time it fetched anything.

JavaScript avoids this with asynchronous functions. Think of ordering food at a counter: you place your order, receive a token number, and step aside — you don't stand frozen at the counter blocking the queue; you can check your phone, talk to a friend, whatever, and come back when your token is called. An async function works the same way: it starts a request, hands back a "token" called a Promise immediately, and the rest of the browser keeps running normally. The keyword await means "pause this specific function here until its token is called" — not "freeze the whole browser." This corrects a common misconception directly: await pauses one function, not the page. Everything else — button clicks, animations, other scripts — keeps working while an awaited request is in flight.

The Code

<script>
  const btn = document.getElementById("getWeatherBtn");
  const result = document.getElementById("result");
  const citySelect = document.getElementById("citySelect");

  btn.addEventListener("click", () => {
    const [lat, lon] = citySelect.value.split(",");
    const cityName = citySelect.options[citySelect.selectedIndex].text;
    fetchWeather(lat, lon, cityName);
  });

  async function fetchWeather(lat, lon, cityName) {
    result.innerHTML = "Loading...";
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,relative_humidity_2m,wind_speed_10m&timezone=auto`;

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error("Server responded with status " + response.status);
      }
      const data = await response.json();
      displayWeather(data, cityName);
    } catch (error) {
      result.innerHTML = `<p class="error">Could not fetch weather: ${error.message}</p>`;
    }
  }

  function displayWeather(data, cityName) {
    const temp = data.current.temperature_2m;
    const humidity = data.current.relative_humidity_2m;
    const wind = data.current.wind_speed_10m;
    result.innerHTML = `
      <h2>${cityName}</h2>
      <div id="temp">${temp}°C</div>
      <p>Humidity: ${humidity}%</p>
      <p>Wind Speed: ${wind} km/h</p>
    `;
  }
</script>

Tracing the Full App, Step by Step

Suppose a student selects "Chennai" from the dropdown and clicks "Get Weather." Trace exactly what the computer does, in order:

  1. The click event fires, running the anonymous function passed to addEventListener.
  2. citySelect.value is the string "13.0827,80.2707" — the value of the currently selected <option>. .split(",") breaks it into an array: ["13.0827", "80.2707"]. Array destructuring — const [lat, lon] = ... — assigns the first element to lat and the second to lon in one line.
  3. cityName is read from the visible text of the selected option — "Chennai" — not the coordinate string.
  4. fetchWeather("13.0827", "80.2707", "Chennai") is called. Because the function is declared async, calling it starts running the function body immediately and synchronously up until the first await.
  5. result.innerHTML = "Loading..." runs immediately — the user sees this text appear on screen right away, before any network activity happens. This matters: without it, the card would sit blank and silent for however long the network takes, and a user might think the button did nothing.
  6. The template literal builds the exact URL string described earlier, substituting in the real lat/lon values.
  7. await fetch(url) starts the network request and pauses this function only. Control returns to the browser, which stays fully responsive.
  8. When Open-Meteo's server replies, execution resumes. response.ok is true if the HTTP status code was in the 200s (success); if the network or server failed, we deliberately throw our own error rather than silently continuing with broken data.
  9. await response.json() parses the response body text into an actual JavaScript object — this step can also take a moment and is itself awaited.
  10. displayWeather(data, "Chennai") runs. It reads data.current.temperature_2m (say, 29.6), data.current.relative_humidity_2m (say, 78), and data.current.wind_speed_10m (say, 11.2).
  11. A new HTML string is built with these values inserted, and assigned to result.innerHTML, which replaces "Loading..." with the finished weather card — headline "Chennai," large "29.6°C," and the two supporting lines.

If step 7 or 8 fails — say, the student has no internet connection — the catch block runs instead, and result.innerHTML is set to a red error message rather than leaving "Loading..." stuck on screen forever. This is not optional decoration; an app that never handles the failure case is not actually finished, because networks fail constantly in real conditions (a train passing through a tunnel, a shared college Wi-Fi dropping packets).

Architecture at a Glance

Browser (Client) HTML + CSS + JS running on your phone or laptop Open-Meteo API Backend + weather model database (runs on someone else's server) ① HTTP GET request ?latitude=13.08&longitude=80.27 ② JSON response { "temperature_2m": 29.6, ... } ③ JavaScript await response.json() builds an HTML string ④ Page updates "Chennai: 29.6°C" appears

When You DO Need Your Own Backend

Open-Meteo works with no API key, which is exactly why it was chosen for this capstone — the frontend can call it directly from the browser. Many real-world APIs, however, require a secret API key to prove which developer is making the request (partly for billing, partly to prevent abuse). This creates a genuine problem: if you put that secret key directly inside your frontend JavaScript, anyone can open the browser's developer tools, read your source code, and steal it — because, remember, the frontend is the one layer every visitor can inspect.

The standard fix is to write a small backend of your own that sits between your frontend and the key-requiring API: your frontend calls your server, your server (which keeps the secret key safely hidden, never sent to the browser) calls the real weather API, and your server relays the answer back. In pseudocode, such a backend route might look like:

// This code would run on a server, NOT in the browser —
// illustrative only, not part of our working app.
app.get("/weather", async (req, res) => {
  const secretKey = process.env.WEATHER_API_KEY; // hidden from users
  const apiResponse = await fetch(
    `https://some-api.example.com/data?key=${secretKey}&city=${req.query.city}`
  );
  const data = await apiResponse.json();
  res.json(data); // send only the weather data back, never the key
});

We don't need this for Bharat Weather, because Open-Meteo has no secret to protect — but recognising when a proxy backend is required, versus when a frontend can safely talk to a public API directly, is exactly the judgment call that separates "wrote some JavaScript" from "understands full-stack architecture."

Building It Up: Difficulty in Stages

If this is your first time combining fetch, async/await, and DOM updates, build the app in this order rather than typing the whole thing at once:

  • Stage 0 — Static mockup: hardcode <div id="temp">29°C</div> directly in the HTML with no JavaScript at all, just to get the CSS card looking right.
  • Stage 1 — One hardcoded city: remove the dropdown, call fetchWeather("28.6139", "77.2090", "Delhi") automatically when the page loads, with no button yet. Confirm real numbers appear.
  • Stage 2 — Add the dropdown and button: wire up citySelect and the click listener so any of the five cities can be chosen, as shown above.
  • Stage 3 — Add error handling: add the try/catch and the "Loading..." message, then deliberately test it by turning off your internet connection and clicking the button — you should see the red error message, not a frozen page.
  • Stage 4 (stretch) — Auto-refresh: wrap the fetch call with setInterval(() => fetchWeather(lat, lon, cityName), 600000) to refresh every ten minutes automatically, matching how a real weather app behaves.

Common Mistakes to Watch For

Beyond the misconceptions already named — assuming full-stack always means writing your own server, and assuming await freezes the whole browser — watch for these specific bugs:

  • Forgetting await before fetch(...). Without it, response holds a Promise object, not the actual reply, and response.ok is undefined rather than true or false — code that looks fine but silently misbehaves.
  • Using string concatenation instead of a template literal for the URL and forgetting a & or =, which produces a URL Open-Meteo can't parse and returns an error response for.
  • innerHTML vs textContent. We used innerHTML because we want the browser to interpret our template as actual HTML tags (<h2>, <div>). This is safe here because every value we insert (temperature, humidity) comes from a trusted numeric API field — but if you ever insert raw text typed by a user into innerHTML, a malicious visitor could type actual HTML/script tags into that text and have the browser execute them. That security bug is called Cross-Site Scripting (XSS). The safe rule: use innerHTML only for content you built yourself or trust completely; use textContent for anything typed directly by a user.
  • Mismatched id attributes between the HTML and the document.getElementById calls — the single most common "nothing happens when I click the button" bug, and it produces no error message at all, so check this first when debugging.

Check Your Understanding

  1. In the JSON example, what does data.current.wind_speed_10m return, and why are there two dots in that expression rather than one?
  2. A classmate writes const response = fetch(url); (no await) and then tries response.json() immediately on the next line. Explain precisely what goes wrong and why.
  3. Why does the app set result.innerHTML = "Loading..."; before calling fetch, rather than after?
  4. Suppose Open-Meteo's server is temporarily down and returns a 500 status code. Trace through the code and state exactly which line detects this and what the user sees on screen.
  5. A friend argues, "This app isn't really full-stack because you didn't write any backend code." Using the three-layer definition from this chapter, explain why they are only partly right.
  6. Why would it be a security mistake to fetch a weather API that requires a secret key directly from this same frontend JavaScript?

Summary

A full-stack application always has three cooperating layers — frontend (what the user sees and touches), backend (a program that processes requests, running elsewhere), and a data layer the backend draws from — and "full stack" describes the skill of making these layers communicate correctly, not necessarily writing every layer's code yourself. In this capstone, the frontend was built from familiar HTML structure and CSS styling, then connected to a real backend (Open-Meteo) using JavaScript's fetch function, which returns data as JSON — a labelled, nested table format read with dot notation like data.current.temperature_2m. Because network requests take unpredictable time, JavaScript handles them asynchronously: async functions and the await keyword pause only the function that's waiting, never the whole browser, which is why the page stays responsive while data is in flight. A finished app also handles the failure case explicitly with try/catch, rather than assuming the network always succeeds. And knowing when a frontend can safely call an API directly versus when it needs its own backend proxy to protect a secret key is the architectural judgment that turns "I can write JavaScript" into "I understand full-stack design."

Think About It

Think about this: How would you explain full stack capstone: building a complete indian weather app to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where full stack capstone: building a complete indian weather app is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting full stack capstone: building a complete indian weather app to at least 3 other topics you have studied.
← Version Control with Git: Never Lose Your Code AgainCSS3 and Responsive Design: Beautiful on Every Screen →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn