š 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.
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
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
requestslibrary 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, usenpm install node-fetch. - C#:
HttpClientis built-in (part ofSystem.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
- Read the API docs (if available) to know what to expect
- Make a request and pretty-print the full response
- Identify the fields you need ā note the nesting path
- Write access code for just those fields
- 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.
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:
- Store keys in environment variables or a .env file
- Add
.envto your .gitignore - 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:
- Prompt the user for a GitHub username
- Fetch their profile from
https://api.github.com/users/{username} - Display: name, bio, location, public repos count, followers count, account creation date
- Handle errors: user not found, network failure
- 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:
- Prompt the user for a city name
- Fetch weather from
https://wttr.in/{city}?format=j1 - Display: temperature (°F and °C), conditions, humidity, wind speed, "feels like" temperature
- Handle errors: invalid city, network failure
- Bonus: Save the last 5 lookups to a
weather_history.jsonfile (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:
- Fetch 5 multiple-choice trivia questions
- Display each question with shuffled answer choices (A, B, C, D)
- Accept the user's answer and check if it's correct
- Track and display the score (e.g., "3/5 correct!")
- Handle HTML entities in the questions (the API returns
&and")
š” 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 (& ā &) 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?