Skip to main content

🌐 Lesson 8.3: Making API Requests

Your programs can now save data to files and load it back. But what if you need data you don't already have — live weather, the latest news, a random joke, or stock prices? That data lives on servers all over the internet, and the way your program asks for it is through an API (Application Programming Interface). In this lesson, you'll learn to make HTTP requests, parse JSON responses, and build programs that pull live data from the web.

šŸŽÆ Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what an API is and how the request/response cycle works
  • Understand HTTP basics — methods (GET, POST), status codes, and headers
  • Make GET requests to fetch data from public APIs
  • Parse JSON responses and extract the data you need
  • Use query parameters to customize API requests
  • Handle API errors — network failures, bad responses, rate limits
  • Understand API keys and why many APIs require them

Estimated Time: 60 minutes

Project: Build a weather lookup tool using a real API

šŸ“‘ In This Lesson

What Is an API?

An API is a way for programs to talk to each other. When you visit a website, your browser makes a request and gets back HTML (a web page). When your program makes a request, it gets back data — usually JSON.

sequenceDiagram participant P as Your Program participant S as API Server P->>S: HTTP Request (GET /weather?city=Portland) S->>S: Process request, look up data S->>P: HTTP Response (JSON data) P->>P: Parse JSON, use the data

Think of an API like a restaurant:

  • The menu = the API documentation (tells you what you can order)
  • Your order = the HTTP request (what you're asking for)
  • The kitchen = the server (processes your request)
  • Your food = the response (the data you get back)

You don't need to know how the kitchen works — you just need to know what's on the menu and how to place an order. That's the power of APIs.

šŸ’” APIs Are Everywhere

  • Weather apps → call a weather API (OpenWeatherMap, WeatherAPI)
  • Social media feeds → call Twitter/Reddit/GitHub APIs
  • Maps and directions → call Google Maps or Mapbox APIs
  • Payment processing → call Stripe or PayPal APIs
  • AI assistants → call OpenAI or Anthropic APIs

If an app on your phone shows data it didn't create itself, it's almost certainly using an API.

šŸŽ“ Instructor Note: Delivery Guidance

This is one of the most exciting lessons in the course — it's where programs start feeling "real" because they connect to the outside world. Start by opening a browser and visiting https://api.github.com/users/github — students will see raw JSON in their browser. Explain: "That's an API response. Your browser made a GET request, and the server sent back JSON instead of a web page. Your program can do the exact same thing." The restaurant analogy works well — students immediately understand the menu/order/kitchen/food metaphor. Emphasize that they've been consuming APIs every time they use an app; now they'll learn how to do it themselves in code.

HTTP Basics

HTTP (HyperText Transfer Protocol) is the language of the web. Every API request uses HTTP. You need to know three things: methods, status codes, and headers.

HTTP Methods

Method Purpose Example
GET Retrieve data (read-only) Get the weather for Portland
POST Send data to create something Create a new user account
PUT Update/replace existing data Update a user's profile
DELETE Remove data Delete a blog post

In this lesson, we'll focus on GET — the most common method and the one you'll use 90% of the time as a beginner.

HTTP Status Codes

Every response includes a status code — a number that tells you whether the request succeeded:

Range Meaning Common Codes
2xx āœ… Success 200 OK — everything worked
3xx ā†Ŗļø Redirect 301 Moved Permanently — try a different URL
4xx āŒ Client Error (your fault) 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests
5xx šŸ’„ Server Error (their fault) 500 Internal Server Error, 503 Service Unavailable

āœ… Quick Memory Aid

  • 2xx = "Here's your data!" (success)
  • 4xx = "You asked wrong." (check your URL, API key, or parameters)
  • 5xx = "We broke." (the server has a problem — try again later)

Anatomy of a URL

flowchart LR A["https://
Protocol"] --> B["api.weather.com
Host (server)"] B --> C["/v1/forecast
Path (endpoint)"] C --> D["?city=Portland&units=metric
Query parameters"] style A fill:#3b82f6,color:#fff style B fill:#6366f1,color:#fff style C fill:#22c55e,color:#fff style D fill:#f59e0b,color:#fff

Making GET Requests

Let's make our first API calls. We'll use free, public APIs that don't require any signup or API key.

import requests  # pip install requests

# --- Basic GET request ---
response = requests.get("https://api.github.com/users/github")

print(response.status_code)   # 200
print(type(response))         # <class 'requests.models.Response'>

# The response body is a JSON string — parse it:
data = response.json()        # Shortcut for json.loads(response.text)
print(data["name"])           # GitHub
print(data["public_repos"])   # (number of public repos)
print(data["bio"])            # How people build software.

# --- You can also get the raw text ---
print(response.text[:100])    # First 100 chars of raw JSON string

# --- Response headers ---
print(response.headers["content-type"])   # application/json; charset=utf-8

# --- Install requests if you haven't ---
# pip install requests
# (or: pip install requests --break-system-packages on some systems)
# 'requests' is not built-in — it's the most popular Python package
// --- Node.js 18+ has built-in fetch! ---
// (Older Node.js: npm install node-fetch)

// Method 1: async/await (recommended)
async function getGitHubUser() {
    const response = await fetch("https://api.github.com/users/github");

    console.log(response.status);     // 200
    console.log(response.ok);         // true (status 200-299)

    const data = await response.json();  // Parse JSON body
    console.log(data.name);              // GitHub
    console.log(data.public_repos);      // (number)
    console.log(data.bio);              // How people build software.
}

getGitHubUser();

// Method 2: .then() chain
fetch("https://api.github.com/users/github")
    .then(response => response.json())
    .then(data => console.log(data.name))
    .catch(err => console.error("Error:", err));

// --- Response headers ---
async function showHeaders() {
    const response = await fetch("https://api.github.com/users/github");
    console.log(response.headers.get("content-type"));
    // application/json; charset=utf-8
}
showHeaders();
using System.Net.Http;
using System.Text.Json;

// --- HttpClient is C#'s built-in HTTP library ---
// Create ONE HttpClient and reuse it (important for performance!)
HttpClient client = new HttpClient();

// Basic GET request:
HttpResponseMessage response = await client.GetAsync(
    "https://api.github.com/users/github"
);
// GitHub API requires a User-Agent header:
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");

Console.WriteLine((int)response.StatusCode);  // 200
Console.WriteLine(response.IsSuccessStatusCode);  // True

// Read and parse JSON:
string jsonText = await response.Content.ReadAsStringAsync();
JsonDocument doc = JsonDocument.Parse(jsonText);
JsonElement root = doc.RootElement;

Console.WriteLine(root.GetProperty("name").GetString());  // GitHub
Console.WriteLine(root.GetProperty("public_repos").GetInt32());

// --- Shortcut: GetStringAsync ---
string json = await client.GetStringAsync(
    "https://api.github.com/users/github"
);
var data = JsonDocument.Parse(json).RootElement;

// --- Deserialize to a class ---
class GitHubUser
{
    public string Name { get; set; } = "";
    public int PublicRepos { get; set; }
    public string? Bio { get; set; }
}

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var user = JsonSerializer.Deserialize<GitHubUser>(json, options);
Console.WriteLine(user?.Name);  // GitHub

āš ļø Setup Requirements

  • Python: Install the requests library first: pip install requests. It's not built-in but is the standard for HTTP in Python.
  • JavaScript: fetch() is built-in for Node.js 18+ and all modern browsers. For older Node.js, use npm install node-fetch.
  • C#: HttpClient is built-in (part of System.Net.Http). No extra packages needed.
šŸŽ“ Instructor Note: Delivery Guidance

Have students run the GitHub API example first — it works without any API key and returns interesting data. If they get a 403 Forbidden error in C#, it's because GitHub requires a User-Agent header. Point out that response.json() (Python/JS) does exactly what json.loads(response.text) would do — it's a convenience method. The key "aha moment" is seeing live data from the internet appear in their terminal. For Python, if students don't have requests installed, they'll get an ImportError — walk through pip install requests and explain that it's the single most-downloaded Python package for a reason. In C#, emphasize creating one HttpClient and reusing it — creating a new one per request is a common beginner mistake that causes connection issues.

Parsing API Responses

API responses are just JSON — you already know how to work with JSON from the last lesson. The new challenge is that API JSON tends to be deeply nested and you need to find the specific fields you care about.

import requests
import json

# Fetch data about a GitHub repository
response = requests.get("https://api.github.com/repos/python/cpython")
repo = response.json()

# Step 1: Explore the response — see what fields are available
# Pretty-print the keys:
print(json.dumps(list(repo.keys()), indent=2))

# Step 2: Extract what you need
print(f"Name: {repo['name']}")                 # cpython
print(f"Stars: {repo['stargazers_count']}")    # (large number)
print(f"Language: {repo['language']}")          # Python
print(f"Open Issues: {repo['open_issues']}")
print(f"Description: {repo['description']}")

# Step 3: Navigate nested data
print(f"Owner: {repo['owner']['login']}")       # python
print(f"Owner Avatar: {repo['owner']['avatar_url']}")

# Step 4: Handle missing fields safely
license_name = repo.get("license", {}).get("name", "Unknown")
print(f"License: {license_name}")

# Pro tip: During development, dump the whole response to see its shape:
# print(json.dumps(repo, indent=2))  # Shows everything, nicely formatted
async function exploreRepo() {
    const response = await fetch("https://api.github.com/repos/nodejs/node");
    const repo = await response.json();

    // Step 1: Explore — see what's available
    console.log(Object.keys(repo));

    // Step 2: Extract what you need
    console.log(`Name: ${repo.name}`);                  // node
    console.log(`Stars: ${repo.stargazers_count}`);
    console.log(`Language: ${repo.language}`);           // JavaScript (or C++)
    console.log(`Open Issues: ${repo.open_issues}`);

    // Step 3: Navigate nested data
    console.log(`Owner: ${repo.owner.login}`);           // nodejs
    console.log(`Owner Avatar: ${repo.owner.avatar_url}`);

    // Step 4: Handle missing fields safely with optional chaining
    console.log(`License: ${repo.license?.name ?? "Unknown"}`);

    // Pro tip: During development, dump everything:
    // console.log(JSON.stringify(repo, null, 2));
}
exploreRepo();
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");

string json = await client.GetStringAsync(
    "https://api.github.com/repos/dotnet/runtime"
);
JsonDocument doc = JsonDocument.Parse(json);
JsonElement repo = doc.RootElement;

// Step 1: Explore — list available properties
foreach (JsonProperty prop in repo.EnumerateObject())
    Console.WriteLine(prop.Name);

// Step 2: Extract what you need
Console.WriteLine($"Name: {repo.GetProperty("name").GetString()}");
Console.WriteLine($"Stars: {repo.GetProperty("stargazers_count").GetInt32()}");
Console.WriteLine($"Language: {repo.GetProperty("language").GetString()}");

// Step 3: Navigate nested data
Console.WriteLine($"Owner: {repo.GetProperty("owner").GetProperty("login").GetString()}");

// Step 4: Handle missing fields safely
string licenseName = "Unknown";
if (repo.TryGetProperty("license", out JsonElement licenseEl) &&
    licenseEl.ValueKind != JsonValueKind.Null &&
    licenseEl.TryGetProperty("name", out JsonElement nameEl))
{
    licenseName = nameEl.GetString() ?? "Unknown";
}
Console.WriteLine($"License: {licenseName}");

šŸ’” The Explore-Then-Extract Workflow

  1. Read the API docs (if available) to know what to expect
  2. Make a request and pretty-print the full response
  3. Identify the fields you need — note the nesting path
  4. Write access code for just those fields
  5. Add safe access (.get(), ?., TryGetProperty) for optional fields

Query Parameters

Most APIs let you customize your request with query parameters — the ?key=value&key2=value2 part at the end of a URL. These filter, sort, or configure what data you get back.

import requests

# --- Method 1: params dictionary (recommended!) ---
# requests builds the URL for you and handles encoding
response = requests.get(
    "https://api.github.com/search/repositories",
    params={
        "q": "language:python stars:>10000",
        "sort": "stars",
        "order": "desc",
        "per_page": 5,
    }
)
data = response.json()

print(f"Total results: {data['total_count']}")
for repo in data["items"]:
    print(f"  ⭐ {repo['stargazers_count']:,}  {repo['full_name']}")
    print(f"     {repo['description'][:60]}...")

# requests built this URL automatically:
print(response.url)
# https://api.github.com/search/repositories?q=language%3Apython+stars%3A%3E10000&sort=stars&...

# --- Method 2: URL string (works but messy) ---
# You'd have to manually encode special characters
url = "https://api.github.com/search/repositories?q=python&sort=stars"
response = requests.get(url)

# Always prefer the params dict — it handles:
# - URL encoding (spaces → %20, : → %3A)
# - Special characters
# - Building the URL correctly
// --- Method 1: URLSearchParams (recommended!) ---
async function searchRepos() {
    const params = new URLSearchParams({
        q: "language:javascript stars:>10000",
        sort: "stars",
        order: "desc",
        per_page: "5",    // URLSearchParams values must be strings
    });

    const url = `https://api.github.com/search/repositories?${params}`;
    const response = await fetch(url);
    const data = await response.json();

    console.log(`Total results: ${data.total_count}`);
    for (let repo of data.items) {
        console.log(`  ⭐ ${repo.stargazers_count.toLocaleString()}  ${repo.full_name}`);
        console.log(`     ${repo.description?.slice(0, 60)}...`);
    }
}
searchRepos();

// --- Method 2: Template literal (simpler for few params) ---
async function searchSimple(language) {
    let url = `https://api.github.com/search/repositories?q=language:${language}&sort=stars&per_page=3`;
    let response = await fetch(url);
    let data = await response.json();
    return data.items;
}

// URLSearchParams handles encoding automatically:
let params = new URLSearchParams({ q: "hello world" });
console.log(params.toString());  // q=hello+world
using System.Web;

HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");

// --- Method 1: UriBuilder + query string ---
var builder = new UriBuilder("https://api.github.com/search/repositories");
var query = HttpUtility.ParseQueryString(string.Empty);
query["q"] = "language:csharp stars:>5000";
query["sort"] = "stars";
query["order"] = "desc";
query["per_page"] = "5";
builder.Query = query.ToString();

string json = await client.GetStringAsync(builder.ToString());
JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;

Console.WriteLine($"Total: {root.GetProperty("total_count").GetInt32()}");
foreach (JsonElement repo in root.GetProperty("items").EnumerateArray())
{
    string name = repo.GetProperty("full_name").GetString()!;
    int stars = repo.GetProperty("stargazers_count").GetInt32();
    Console.WriteLine($"  ⭐ {stars:N0}  {name}");
}

// --- Method 2: String interpolation (simpler cases) ---
string lang = "csharp";
string url = $"https://api.github.com/search/repositories?q=language:{lang}&sort=stars";
string simpleJson = await client.GetStringAsync(url);

šŸ’” Always Use Parameter Builders

Never manually concatenate query strings with + or f-strings for user input. If someone searches for "C# tips & tricks", the & and # would break the URL. Parameter builders (params={}, URLSearchParams, HttpUtility.ParseQueryString) automatically encode special characters.

Handling API Errors

API requests can fail in many ways: the server might be down, your internet might be offline, the API might reject your request, or you might hit a rate limit. Robust code handles all of these.

import requests

def fetch_user(username):
    """Fetch a GitHub user's profile with full error handling."""
    url = f"https://api.github.com/users/{username}"

    try:
        response = requests.get(url, timeout=10)  # 10-second timeout

        # Check for HTTP errors (4xx and 5xx)
        if response.status_code == 404:
            print(f"User '{username}' not found.")
            return None
        elif response.status_code == 403:
            print("Access forbidden — you may have hit the rate limit.")
            print(f"Rate limit resets at: {response.headers.get('X-RateLimit-Reset')}")
            return None
        elif response.status_code != 200:
            print(f"Error: HTTP {response.status_code}")
            return None

        return response.json()

    except requests.exceptions.ConnectionError:
        print("Error: Could not connect. Check your internet connection.")
        return None
    except requests.exceptions.Timeout:
        print("Error: Request timed out. The server may be slow.")
        return None
    except requests.exceptions.JSONDecodeError:
        print("Error: Response was not valid JSON.")
        return None
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")
        return None

# Usage:
user = fetch_user("octocat")
if user:
    print(f"Found: {user['name']} ({user['login']})")

user = fetch_user("this-user-definitely-does-not-exist-12345")
# User 'this-user-definitely-does-not-exist-12345' not found.
async function fetchUser(username) {
    const url = `https://api.github.com/users/${username}`;

    try {
        // AbortController for timeout
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 10000);

        const response = await fetch(url, { signal: controller.signal });
        clearTimeout(timeoutId);

        // Check HTTP status
        if (response.status === 404) {
            console.log(`User '${username}' not found.`);
            return null;
        }
        if (response.status === 403) {
            console.log("Access forbidden — rate limit may be hit.");
            return null;
        }
        if (!response.ok) {
            console.log(`Error: HTTP ${response.status}`);
            return null;
        }

        return await response.json();

    } catch (err) {
        if (err.name === "AbortError") {
            console.log("Error: Request timed out.");
        } else if (err.name === "TypeError") {
            // fetch throws TypeError for network failures
            console.log("Error: Network failure. Check your connection.");
        } else {
            console.log(`Error: ${err.message}`);
        }
        return null;
    }
}

// Usage:
let user = await fetchUser("octocat");
if (user) console.log(`Found: ${user.name} (${user.login})`);

await fetchUser("this-user-definitely-does-not-exist-12345");
// User 'this-user-definitely-does-not-exist-12345' not found.
using System.Net.Http;
using System.Text.Json;

HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
client.Timeout = TimeSpan.FromSeconds(10);

async Task<JsonElement?> FetchUser(string username)
{
    string url = $"https://api.github.com/users/{username}";

    try
    {
        HttpResponseMessage response = await client.GetAsync(url);

        if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            Console.WriteLine($"User '{username}' not found.");
            return null;
        }
        if (response.StatusCode == System.Net.HttpStatusCode.Forbidden)
        {
            Console.WriteLine("Access forbidden — rate limit may be hit.");
            return null;
        }
        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine($"Error: HTTP {(int)response.StatusCode}");
            return null;
        }

        string json = await response.Content.ReadAsStringAsync();
        return JsonDocument.Parse(json).RootElement;
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("Error: Request timed out.");
        return null;
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
        return null;
    }
}

// Usage:
var user = await FetchUser("octocat");
if (user.HasValue)
    Console.WriteLine($"Found: {user.Value.GetProperty("name").GetString()}");

āœ… API Error Handling Checklist

  • Set a timeout — don't let requests hang forever (10 seconds is reasonable)
  • Check the status code — handle 404, 403, and other expected errors specifically
  • Catch network errors — no internet, DNS failure, server unreachable
  • Catch JSON parse errors — the response might not be valid JSON
  • Return null/None on failure — let the caller decide what to do
  • Log meaningful messages — "User not found" is better than a raw stack trace

API Keys and Authentication

Many APIs require an API key — a unique code that identifies you. It's like a library card: free to get, but required to borrow books. API keys let the provider track usage, enforce rate limits, and block abuse.

flowchart LR A["1. Sign up
on the API website"] --> B["2. Get your
API key"] B --> C["3. Include key
in your requests"] C --> D["4. API server
validates key"] style A fill:#3b82f6,color:#fff style B fill:#6366f1,color:#fff style C fill:#f59e0b,color:#fff style D fill:#22c55e,color:#fff
import requests
import os

# --- NEVER hard-code your API key! ---
# āŒ Bad: api_key = "abc123secret"  — anyone reading your code sees it!

# āœ… Good: Use an environment variable
api_key = os.environ.get("WEATHER_API_KEY")
if not api_key:
    print("Error: Set the WEATHER_API_KEY environment variable.")
    print("  export WEATHER_API_KEY=your_key_here  (Mac/Linux)")
    print("  set WEATHER_API_KEY=your_key_here      (Windows)")
    exit(1)

# Method 1: API key as a query parameter (most common)
response = requests.get(
    "https://api.weatherapi.com/v1/current.json",
    params={
        "key": api_key,
        "q": "Portland",
    }
)

# Method 2: API key in a header (some APIs prefer this)
response = requests.get(
    "https://api.example.com/data",
    headers={
        "Authorization": f"Bearer {api_key}",
        # or: "X-API-Key": api_key
    }
)

# --- Using a .env file (popular approach) ---
# 1. Create a file called .env:
#    WEATHER_API_KEY=abc123secret
#
# 2. Install python-dotenv: pip install python-dotenv
#
# 3. Load it in your code:
# from dotenv import load_dotenv
# load_dotenv()
# api_key = os.environ["WEATHER_API_KEY"]
#
# 4. Add .env to .gitignore so it's never committed!
// --- NEVER hard-code your API key! ---
// āŒ Bad: const apiKey = "abc123secret";

// āœ… Good: Use an environment variable
const apiKey = process.env.WEATHER_API_KEY;
if (!apiKey) {
    console.log("Error: Set the WEATHER_API_KEY environment variable.");
    process.exit(1);
}

// Method 1: API key as a query parameter
const params = new URLSearchParams({
    key: apiKey,
    q: "Portland",
});
const response = await fetch(
    `https://api.weatherapi.com/v1/current.json?${params}`
);

// Method 2: API key in a header
const response2 = await fetch("https://api.example.com/data", {
    headers: {
        "Authorization": `Bearer ${apiKey}`,
        // or: "X-API-Key": apiKey
    }
});

// --- Using a .env file with dotenv ---
// 1. Create .env file:  WEATHER_API_KEY=abc123secret
// 2. npm install dotenv
// 3. At top of your script:
//    require("dotenv").config();
//    const apiKey = process.env.WEATHER_API_KEY;
// 4. Add .env to .gitignore!
// --- NEVER hard-code your API key! ---
// āŒ Bad: string apiKey = "abc123secret";

// āœ… Good: Use an environment variable
string? apiKey = Environment.GetEnvironmentVariable("WEATHER_API_KEY");
if (string.IsNullOrEmpty(apiKey))
{
    Console.WriteLine("Error: Set the WEATHER_API_KEY environment variable.");
    return;
}

HttpClient client = new HttpClient();

// Method 1: API key as a query parameter
string url = $"https://api.weatherapi.com/v1/current.json?key={apiKey}&q=Portland";
string json = await client.GetStringAsync(url);

// Method 2: API key in a header
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
// or: client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
string json2 = await client.GetStringAsync("https://api.example.com/data");

// --- Using appsettings.json or User Secrets ---
// For .NET projects, use the Secret Manager:
// dotnet user-secrets init
// dotnet user-secrets set "WeatherApiKey" "abc123secret"
// Then read with IConfiguration in your app.

āš ļø Never Commit API Keys to Git!

If you push an API key to GitHub, bots will find it within minutes and use it. Always:

  1. Store keys in environment variables or a .env file
  2. Add .env to your .gitignore
  3. If you accidentally commit a key, revoke it immediately and generate a new one
šŸŽ“ Instructor Note: Delivery Guidance

API key security is worth spending extra time on — leaked keys are a real and common problem. Show a news article about a company that was charged thousands of dollars because a developer committed an AWS key to GitHub. For the exercises, recommend wttr.in (no key required) or have students sign up for a free WeatherAPI key (free tier, no credit card). The .env file pattern is industry standard — even for a beginner course, it's worth teaching the right habit early. Students who go on to build real applications will thank you.

Working with Real APIs

Let's put it all together with some free public APIs. These examples don't require API keys.

Example 1: Random Jokes

import requests

def get_joke():
    response = requests.get(
        "https://official-joke-api.appspot.com/random_joke",
        timeout=10
    )
    if response.status_code != 200:
        return "Couldn't fetch a joke right now."

    joke = response.json()
    return f"{joke['setup']}\n...{joke['punchline']}"

print(get_joke())
# Why don't scientists trust atoms?
# ...Because they make up everything!
async function getJoke() {
    const response = await fetch(
        "https://official-joke-api.appspot.com/random_joke"
    );
    if (!response.ok) return "Couldn't fetch a joke right now.";

    const joke = await response.json();
    return `${joke.setup}\n...${joke.punchline}`;
}

console.log(await getJoke());
HttpClient client = new HttpClient();
string json = await client.GetStringAsync(
    "https://official-joke-api.appspot.com/random_joke"
);
var joke = JsonDocument.Parse(json).RootElement;
Console.WriteLine(joke.GetProperty("setup").GetString());
Console.WriteLine($"...{joke.GetProperty("punchline").GetString()}");

Example 2: Weather (Text-Based, No Key)

import requests

def get_weather(city):
    """Fetch weather using wttr.in (no API key needed!)"""
    # ?format=j1 returns JSON instead of the ASCII art version
    response = requests.get(
        f"https://wttr.in/{city}",
        params={"format": "j1"},
        timeout=10
    )
    if response.status_code != 200:
        print(f"Could not get weather for {city}")
        return

    data = response.json()
    current = data["current_condition"][0]

    temp_f = current["temp_F"]
    temp_c = current["temp_C"]
    desc = current["weatherDesc"][0]["value"]
    humidity = current["humidity"]
    wind = current["windspeedMiles"]

    print(f"\nšŸŒ¤ļø Weather in {city}:")
    print(f"   Temperature: {temp_f}°F ({temp_c}°C)")
    print(f"   Conditions:  {desc}")
    print(f"   Humidity:    {humidity}%")
    print(f"   Wind:        {wind} mph")

get_weather("Portland")
get_weather("Tokyo")
get_weather("London")
async function getWeather(city) {
    const params = new URLSearchParams({ format: "j1" });
    const response = await fetch(`https://wttr.in/${city}?${params}`);

    if (!response.ok) {
        console.log(`Could not get weather for ${city}`);
        return;
    }

    const data = await response.json();
    const current = data.current_condition[0];

    console.log(`\nšŸŒ¤ļø Weather in ${city}:`);
    console.log(`   Temperature: ${current.temp_F}°F (${current.temp_C}°C)`);
    console.log(`   Conditions:  ${current.weatherDesc[0].value}`);
    console.log(`   Humidity:    ${current.humidity}%`);
    console.log(`   Wind:        ${current.windspeedMiles} mph`);
}

await getWeather("Portland");
await getWeather("Tokyo");
HttpClient client = new HttpClient();

async Task GetWeather(string city)
{
    string url = $"https://wttr.in/{city}?format=j1";
    string json = await client.GetStringAsync(url);
    var root = JsonDocument.Parse(json).RootElement;
    var current = root.GetProperty("current_condition")[0];

    Console.WriteLine($"\nšŸŒ¤ļø Weather in {city}:");
    Console.WriteLine($"   Temp: {current.GetProperty("temp_F").GetString()}°F");
    Console.WriteLine($"   Conditions: {current.GetProperty("weatherDesc")[0].GetProperty("value").GetString()}");
    Console.WriteLine($"   Humidity: {current.GetProperty("humidity").GetString()}%");
}

await GetWeather("Portland");
await GetWeather("Tokyo");

Free Public APIs for Practice

API URL Key? Returns
GitHub api.github.com No User profiles, repos, search
wttr.in wttr.in/{city}?format=j1 No Weather data
Joke API official-joke-api.appspot.com No Random jokes
JSONPlaceholder jsonplaceholder.typicode.com No Fake REST API for testing
Open Trivia DB opentdb.com/api.php No Trivia questions
WeatherAPI api.weatherapi.com Yes (free) Detailed weather + forecast

Exercises

šŸ‹ļø Exercise 1: GitHub Profile Viewer

Objective: Build a program that takes a GitHub username as input and displays a formatted profile card with their info.

Requirements:

  1. Prompt the user for a GitHub username
  2. Fetch their profile from https://api.github.com/users/{username}
  3. Display: name, bio, location, public repos count, followers count, account creation date
  4. Handle errors: user not found, network failure
  5. Allow looking up multiple users without restarting

Expected output:

Enter GitHub username (or 'quit'): octocat

╔══════════════════════════════════╗
  šŸ‘¤  The Octocat (@octocat)
  šŸ“  San Francisco
  šŸ“  (no bio)
  šŸ“¦  8 public repos
  šŸ‘„  12,345 followers
  šŸ“…  Joined: 2011-01-25
ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
āœ… Solution
import requests

def show_profile(username):
    response = requests.get(
        f"https://api.github.com/users/{username}",
        timeout=10
    )

    if response.status_code == 404:
        print(f"User '{username}' not found.")
        return
    if response.status_code != 200:
        print(f"Error: HTTP {response.status_code}")
        return

    u = response.json()
    name = u.get("name") or "(no name)"
    bio = u.get("bio") or "(no bio)"
    location = u.get("location") or "(unknown)"
    repos = u.get("public_repos", 0)
    followers = u.get("followers", 0)
    joined = u.get("created_at", "")[:10]

    print(f"\n╔══════════════════════════════════╗")
    print(f"  šŸ‘¤  {name} (@{u['login']})")
    print(f"  šŸ“  {location}")
    print(f"  šŸ“  {bio[:50]}")
    print(f"  šŸ“¦  {repos} public repos")
    print(f"  šŸ‘„  {followers:,} followers")
    print(f"  šŸ“…  Joined: {joined}")
    print(f"ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n")

while True:
    username = input("Enter GitHub username (or 'quit'): ").strip()
    if username.lower() == "quit":
        break
    if username:
        show_profile(username)
const readline = require("readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = (q) => new Promise(r => rl.question(q, r));

async function showProfile(username) {
    const response = await fetch(`https://api.github.com/users/${username}`);
    if (response.status === 404) { console.log(`User '${username}' not found.`); return; }
    if (!response.ok) { console.log(`Error: HTTP ${response.status}`); return; }

    const u = await response.json();
    console.log(`\n╔══════════════════════════════════╗`);
    console.log(`  šŸ‘¤  ${u.name ?? "(no name)"} (@${u.login})`);
    console.log(`  šŸ“  ${u.location ?? "(unknown)"}`);
    console.log(`  šŸ“  ${(u.bio ?? "(no bio)").slice(0, 50)}`);
    console.log(`  šŸ“¦  ${u.public_repos} public repos`);
    console.log(`  šŸ‘„  ${u.followers.toLocaleString()} followers`);
    console.log(`  šŸ“…  Joined: ${u.created_at?.slice(0, 10)}`);
    console.log(`ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n`);
}

async function main() {
    while (true) {
        let name = (await prompt("GitHub username (or 'quit'): ")).trim();
        if (name === "quit") { rl.close(); break; }
        if (name) await showProfile(name);
    }
}
main();
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "GitHubProfileViewer");

while (true)
{
    Console.Write("GitHub username (or 'quit'): ");
    string? username = Console.ReadLine()?.Trim();
    if (username == "quit" || string.IsNullOrEmpty(username)) break;

    var resp = await client.GetAsync($"https://api.github.com/users/{username}");
    if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
    { Console.WriteLine($"User '{username}' not found."); continue; }

    string json = await resp.Content.ReadAsStringAsync();
    var u = JsonDocument.Parse(json).RootElement;

    string name = u.GetProperty("name").ValueKind == JsonValueKind.Null
        ? "(no name)" : u.GetProperty("name").GetString()!;

    Console.WriteLine($"\n╔══════════════════════════════════╗");
    Console.WriteLine($"  šŸ‘¤  {name} (@{u.GetProperty("login").GetString()})");
    Console.WriteLine($"  šŸ“¦  {u.GetProperty("public_repos").GetInt32()} public repos");
    Console.WriteLine($"  šŸ‘„  {u.GetProperty("followers").GetInt32():N0} followers");
    Console.WriteLine($"ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n");
}

šŸ‹ļø Exercise 2: Weather Dashboard

Objective: Build a weather lookup tool that lets the user check the weather for any city. Use the wttr.in API (no key required).

Requirements:

  1. Prompt the user for a city name
  2. Fetch weather from https://wttr.in/{city}?format=j1
  3. Display: temperature (°F and °C), conditions, humidity, wind speed, "feels like" temperature
  4. Handle errors: invalid city, network failure
  5. Bonus: Save the last 5 lookups to a weather_history.json file (combining Lessons 22, 23, and 24!)
šŸ’” Hints

The wttr.in JSON response has this structure: data["current_condition"][0] contains the current weather. Fields include temp_F, temp_C, FeelsLikeF, humidity, windspeedMiles, and weatherDesc[0]["value"]. For the bonus, use the Load → Modify → Save pattern from Lesson 23.

šŸ‹ļø Exercise 3: Trivia Game

Objective: Build a trivia game using the Open Trivia Database API. The game should ask 5 questions, track the score, and show the final result.

API endpoint: https://opentdb.com/api.php?amount=5&type=multiple

Requirements:

  1. Fetch 5 multiple-choice trivia questions
  2. Display each question with shuffled answer choices (A, B, C, D)
  3. Accept the user's answer and check if it's correct
  4. Track and display the score (e.g., "3/5 correct!")
  5. Handle HTML entities in the questions (the API returns &amp; and &quot;)
šŸ’” Hints

The API response has results array. Each result has question, correct_answer, and incorrect_answers (an array of 3). Combine correct + incorrect, shuffle them, and display as A/B/C/D. Python: use html.unescape() for HTML entities. JavaScript: create a simple function or use a regex. The shuffle is important — if the correct answer is always first, it's too easy!

šŸŽ“ Instructor Note: Delivery Guidance

Exercise 1 (GitHub Profile) is the warmup and works great as a live-coding demo. Exercise 2 (Weather Dashboard) is the main project — the bonus of saving history to JSON ties together all three lessons in Module 8. Exercise 3 (Trivia Game) is the most fun and makes an excellent capstone for this module — students build a playable game that pulls live data from the internet. The HTML entity decoding (&amp; → &) is a real-world data-cleaning challenge. If students struggle, suggest they focus on exercises 1 and 2 first and treat exercise 3 as an optional challenge. All three exercises use APIs that work without API keys, so there's no setup friction.

Summary

šŸŽ‰ Key Takeaways

  • APIs let your programs fetch live data from servers across the internet
  • HTTP GET requests retrieve data; the response includes a status code and a body (usually JSON)
  • Status codes: 2xx = success, 4xx = you made an error, 5xx = server error
  • Query parameters customize your request — use parameter builders, not string concatenation
  • Always handle errors: network failures, timeouts, bad status codes, invalid JSON
  • API keys identify you to the server — store them in environment variables, never in code
  • The workflow: read docs → make request → explore response → extract fields → handle errors

Cross-Language Comparison

Operation šŸ Python ⚔ JavaScript šŸ”· C#
HTTP library requests (pip install) fetch() (built-in) HttpClient (built-in)
GET request requests.get(url) await fetch(url) await client.GetAsync(url)
Parse JSON response response.json() await response.json() JsonDocument.Parse(text)
Status code response.status_code response.status (int)response.StatusCode
Success check status_code == 200 response.ok response.IsSuccessStatusCode
Query params params={...} URLSearchParams HttpUtility.ParseQueryString
Env variable os.environ.get(key) process.env.KEY Environment.GetEnvironmentVariable

šŸš€ What's Next?

That wraps up Module 8: Working with Files & APIs! You can now read/write files, work with JSON, and fetch live data from the internet. In Module 9, you'll bring everything together in a Capstone Project — planning, building, and reviewing a complete program that uses the skills from the entire course.

šŸŽÆ Quick Check

Question 1: What does HTTP status code 404 mean?

Question 2: Why should you NEVER hard-code API keys in your source code?

Question 3: What HTTP method should you use to retrieve data from an API?