Back to blog
January 15, 2025

Understanding API Calls: Building a Weather App

A real developer explanation of how API calls work, from fetch to state management. No BS, just practical code.

Understanding API Calls: Building a Weather App

How API Calls Actually Work: A Real Dev Explanation

The Basics (No BS Version)

So you want to understand how those weather API calls work? Let me break it down the way I wish someone had explained it to me when I started.


The Complete Flow

Step 1: User Picks a City

Pretty straightforward. User clicks a button in the modal:

<button onClick={() => {
  setSelectedCity(city); // Store the new city
  setShowCitySelector(false); // Close modal
}}>

Nothing fancy. Just updating state. But here's where it gets interesting...



Step 2: useEffect Wakes Up

This is the magic part. React has this thing called useEffect that watches variables:

useEffect(() => {
  fetchWeather(selectedCity);
}, [selectedCity]);

That array at the end? [selectedCity] - that's the dependency list. It's literally React saying:

"Hey, I'm watching selectedCity. If it changes, I'll run fetchWeather again."

Think of it like a guard dog. It sits there watching one thing. When that thing moves, it barks (runs your function).


Step 3: Building the API Request

Here's where we actually talk to the weather API:

const fetchWeather = async (city: City) => {
  // Turn on the loading spinner
  setLoading(true);
  setError(''); // Clear old errors
  
  try {
    // Build the URL with all the data we need
    const url = `https://api.open-meteo.com/v1/forecast?` +
                `latitude=${city.lat}&` +
                `longitude=${city.lon}&` +
                `current=temperature_2m,relative_humidity_2m,...&` +
                `daily=temperature_2m_max,temperature_2m_min,...&` +
                `timezone=auto`;
    
    // Make the actual request
    const response = await fetch(url);
    
    // Did it work?
    if (!response.ok) {
      throw new Error('API said no');
    }
    
    // Convert response to JavaScript object
    const data = await response.json();
    
    // Save it to state
    setWeather({ 
      ...data,
      cityName: city.name,
      country: city.country
    });
    
  } catch (err) {
    console.error('Error:', err);
    setError('Could not get weather data');
  } finally {
    // Always turn off loading, error or not
    setLoading(false);
  }
};

Let me explain what's happening here:



The URL Construction

You're basically building a string with all the parameters the API needs:

  • latitude and longitude - where on Earth we're asking about
  • current - what current weather data we want (temp, humidity, etc.)
  • daily - what forecast data we want (7-day max/min temps)
  • timezone=auto - let the API figure out the timezone



The fetch() Call

fetch() is JavaScript's built-in way to make HTTP requests. It's async, meaning it takes time. That's why we use await - it pauses the function until the API responds.

Without await, your code would keep running while waiting for the response, and you'd try to use data that doesn't exist yet. Bad times.



The response.json() Part

The API sends back text (specifically JSON format). response.json() converts that text into a JavaScript object you can actually work with.

// What the API sends (text):
'{"temperature": 18.5}'

// What response.json() gives you (object):
{ temperature: 18.5 }



Error Handling

The try/catch/finally block is crucial:

  • try: "Attempt this risky stuff"
  • catch: "If anything fails, run this instead"
  • finally: "No matter what happened, do this at the end"

This is why setLoading(false) is in finally - we want to hide the spinner whether we succeeded or failed.


What the API Actually Returns

When you hit that endpoint, you get back something like this:

{
  "latitude": 40.4168,
  "longitude": -3.7038,
  "current": {
    "time": "2024-12-04T15:00",
    "temperature_2m": 18.5,
    "apparent_temperature": 16.2,
    "relative_humidity_2m": 65,
    "precipitation": 0,
    "weather_code": 2,
    "wind_speed_10m": 12.5,
    "pressure_msl": 1015.3
  },
  "daily": {
    "time": ["2024-12-04", "2024-12-05", "2024-12-06", "..."],
    "temperature_2m_max": [20, 22, 19, 21, 23, 20, 18],
    "temperature_2m_min": [12, 14, 11, 13, 15, 12, 10],
    "weather_code": [2, 0, 3, 1, 2, 3, 61]
  }
}

See how the daily data is arrays? That's 7 days of forecasts. Index 0 is today, index 1 is tomorrow, etc.


The Visual Flow

User clicks "Barcelona"
    ↓
setSelectedCity(barcelona) updates state
    ↓
useEffect detects the change
    ↓
fetchWeather(barcelona) runs
    ↓
setLoading(true) - spinner appears
    ↓
Build the URL with Barcelona's coordinates
    ↓
fetch(url) - send HTTP request, WAIT for response
    ↓
API responds with JSON data
    ↓
response.json() - convert to JavaScript object
    ↓
setWeather(data) - save to state
    ↓
setLoading(false) - hide spinner
    ↓
React re-renders with new data
    ↓
User sees Barcelona's weather


Key Concepts You Need to Get

React State (useState)

const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');

State is how React remembers things between renders. When you call setWeather(), React:

  1. Updates the value
  2. Re-renders the component
  3. Shows the new data

It's not like a normal variable. Regular variables reset on every render. State persists.


The useEffect Hook

This one trips people up. Here's the deal:

useEffect(() => {
  // This runs after the component renders
  fetchWeather(selectedCity);
}, [selectedCity]); // Only re-run if this changes

When does it run?

  1. First render (always)
  2. Anytime selectedCity changes

Why use it?

Because fetching data is a "side effect" - something that affects the outside world (the API server). React wants you to declare these explicitly.


async/await Explained

// Without await (wrong):
const response = fetch(url);
console.log(response); // Promise { <pending> } - not what you want

// With await (right):
const response = await fetch(url);
console.log(response); // Response object with actual data

await literally pauses your function until the promise resolves. But you can only use await inside an async function, which is why we write:

const fetchWeather = async (city: City) => {
  // Now we can use await inside here
}


Why This Pattern Works

Separation of Concerns

  • fetchWeather only fetches data
  • useEffect only watches for changes
  • State only stores data
  • UI only displays data

Everything has one job. Easy to debug, easy to change.


Predictable State

You always know what's happening:

  • loading === true → show spinner
  • error !== '' → show error
  • weather !== null → show weather

No weird in-between states.


Automatic Updates

Change the city? React handles the rest:

  1. useEffect triggers
  2. Data fetches
  3. State updates
  4. UI re-renders

You don't manually update the DOM anywhere. React does it.


A Simpler Example

If all that was too much, here's the absolute minimal version:

// 1. User picks a city
const city = { lat: 40.4168, lon: -3.7038, name: 'Madrid' };

// 2. Build URL
const url = `https://api.open-meteo.com/v1/forecast?latitude=40.4168&longitude=-3.7038&current=temperature_2m`;

// 3. Fetch data
const response = await fetch(url);
const data = await response.json();

// 4. Show it
console.log(data.current.temperature_2m); // 18.5

// 5. Render
return <div>{data.current.temperature_2m}°</div>

That's it. Everything else is just handling errors, loading states, and making it production-ready.


Common Mistakes I Made

1. Forgetting async/await

// Wrong - fetch returns a promise, not data
const data = fetch(url).json();

// Right - wait for each step
const response = await fetch(url);
const data = await response.json();


2. Not checking response.ok

// Wrong - assumes it always works
const data = await response.json();

// Right - check for errors
if (!response.ok) throw new Error('Failed');
const data = await response.json();


3. Putting fetch in the component body

// Wrong - runs on every render, infinite loop
function Component() {
  fetch(url); // DON'T DO THIS
  return <div>...</div>
}

// Right - inside useEffect
function Component() {
  useEffect(() => {
    fetch(url);
  }, []);
}

Why APIs Use Coordinates Instead of City Names,
You might wonder: why not just send the city name?
Accuracy: "Paris" could be Paris, France or Paris, Texas. Coordinates are exact.
No Translation: Works in any language without conversion.
Flexibility: Can get weather for ANY point on Earth, not just named cities.
Speed: The API doesn't need to look up city names in a database first.
Your frontend keeps the city names for display. The API just wants lat/lon.
Final Thoughts
API calls seem complex at first, but it's always the same pattern:

1 - Build URL with parameters
2 - Send request (fetch)
3 - Wait for response (await)
4 - Parse response (json)
5 - Update state
6 - React re-renders

Once you get this pattern, you can call any API. Weather, GitHub, Stripe, whatever. The only difference is what parameters you send and what data comes back. The key is understanding that it's all asynchronous. You send a request, wait for a response, then handle that response. Like ordering food - you don't stand at the counter waiting, you sit down and do other stuff until it arrives. Keep building, keep breaking things, and eventually this becomes second nature.

PD: This app was made in just one page.tsx, next steps are like always;
Performance (server components), Architecture (split into components), UI/UX (visual stuffs), Code (cache functions), New features (ur mind ur ideas), Scalability (less call to server, testing) Perhaps will work on it if I'm too bored but got a daugther :)

Back to blog