Skip to content

Commit 1bcf673

Browse files
travisjneumanclaude
andcommitted
feat: add Module 03 — REST APIs: Consuming
Five progressive projects teaching REST API consumption with Python requests library against JSONPlaceholder. Covers GET/POST requests, query parameters, error handling with retries, and building a reusable API client class with requests.Session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 32d8d44 commit 1bcf673

18 files changed

Lines changed: 1250 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Module 03 / Project 01 — First API Call
2+
3+
[README](../../../../README.md)
4+
5+
## Focus
6+
7+
- Making a GET request with `requests.get()`
8+
- Understanding response objects (status code, headers, body)
9+
- Parsing JSON responses into Python dictionaries
10+
11+
## Why this project exists
12+
13+
Before you can build anything that talks to the internet, you need to understand the basic request-response cycle. This project strips it down to the simplest possible case: fetch one resource, look at what comes back, and pull out the pieces you care about.
14+
15+
## Run
16+
17+
```bash
18+
cd projects/modules/03-rest-apis/01-first-api-call
19+
python project.py
20+
```
21+
22+
## Expected output
23+
24+
```text
25+
--- Raw JSON response ---
26+
{
27+
"userId": 1,
28+
"id": 1,
29+
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
30+
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
31+
}
32+
33+
--- Accessing individual fields ---
34+
Status code : 200
35+
Post ID : 1
36+
User ID : 1
37+
Title : sunt aut facere repellat provident occaecati excepturi optio reprehenderit
38+
Body preview: quia et suscipit...
39+
40+
--- Response headers (selected) ---
41+
Content-Type: application/json; charset=utf-8
42+
```
43+
44+
## Alter it
45+
46+
1. Change the URL to fetch post `/posts/5` instead of `/posts/1`. Run again and compare the output.
47+
2. Print the full `body` field instead of just the first 20 characters.
48+
3. Add a line that prints the number of characters in the title.
49+
50+
## Break it
51+
52+
1. Change the URL to `https://jsonplaceholder.typicode.com/posts/99999` (a post that does not exist). What status code do you get? What does `response.json()` return?
53+
2. Remove the `.json()` call and print `response.text` instead. What is the difference?
54+
3. Change the URL to an invalid domain like `https://not-a-real-domain-xyz.com/posts/1`. What error do you get?
55+
56+
## Fix it
57+
58+
1. Add a check: if `response.status_code` is not 200, print a warning and skip the JSON parsing.
59+
2. Wrap the `requests.get()` call in a `try/except` block that catches `requests.exceptions.ConnectionError` and prints a friendly message instead of a traceback.
60+
3. After fixing, run with both a valid and invalid URL to confirm both paths work.
61+
62+
## Explain it
63+
64+
1. What does `response.json()` actually do under the hood?
65+
2. What is the difference between `response.text` and `response.json()`?
66+
3. Why does `requests.get()` return a Response object instead of just the data?
67+
4. What HTTP method does `requests.get()` use, and what does that method mean?
68+
69+
## Mastery check
70+
71+
You can move on when you can:
72+
73+
- write a GET request from memory without looking at docs,
74+
- explain what a status code of 200 means vs 404,
75+
- extract any field from a JSON response,
76+
- describe the difference between `.text` and `.json()`.
77+
78+
## Next
79+
80+
Continue to [Query Parameters](../02-query-parameters/).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Notes — First API Call
2+
3+
## What I learned
4+
5+
6+
## What confused me
7+
8+
9+
## What I want to explore next
10+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Module 03 / Project 01 — First API Call.
2+
3+
Learn how to fetch data from a REST API using requests.get(),
4+
inspect the response object, and parse JSON into a Python dictionary.
5+
"""
6+
7+
# requests is the most popular HTTP library for Python.
8+
# You installed it with: pip install requests
9+
import requests
10+
11+
# json is part of the standard library. We use it here only for
12+
# pretty-printing (indent=2), not for parsing — requests handles that.
13+
import json
14+
15+
16+
def fetch_single_post():
17+
"""Fetch one post from JSONPlaceholder and explore the response."""
18+
19+
# The URL points to a single post resource.
20+
# JSONPlaceholder provides 100 fake posts at /posts/1 through /posts/100.
21+
url = "https://jsonplaceholder.typicode.com/posts/1"
22+
23+
# requests.get() sends an HTTP GET request and returns a Response object.
24+
# The Response object contains everything the server sent back:
25+
# status code, headers, body, encoding, and more.
26+
response = requests.get(url)
27+
28+
# --- Raw JSON response ---
29+
# response.json() parses the response body from a JSON string into
30+
# a Python dictionary (or list, depending on what the API returns).
31+
# This only works if the server actually sent JSON — otherwise it raises
32+
# a JSONDecodeError.
33+
data = response.json()
34+
35+
print("--- Raw JSON response ---")
36+
# json.dumps() converts the dictionary BACK into a formatted string.
37+
# indent=2 makes it human-readable. This is just for display.
38+
print(json.dumps(data, indent=2))
39+
40+
# --- Accessing individual fields ---
41+
# Once parsed, `data` is a plain Python dict. You access fields with
42+
# square brackets, just like any dictionary.
43+
print("\n--- Accessing individual fields ---")
44+
print("Status code :", response.status_code)
45+
print("Post ID :", data["id"])
46+
print("User ID :", data["userId"])
47+
print("Title :", data["title"])
48+
49+
# Slice the body to show just the first 20 characters as a preview.
50+
print("Body preview:", data["body"][:20] + "...")
51+
52+
# --- Response headers ---
53+
# The server sends metadata in HTTP headers. response.headers is a
54+
# case-insensitive dictionary. Content-Type tells you the format of
55+
# the response body.
56+
print("\n--- Response headers (selected) ---")
57+
print("Content-Type:", response.headers.get("Content-Type"))
58+
59+
60+
# This guard ensures the code below only runs when you execute this file
61+
# directly (python project.py). If another file imports this module,
62+
# the code below will NOT run. This is a Python best practice.
63+
if __name__ == "__main__":
64+
fetch_single_post()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Module 03 / Project 02 — Query Parameters
2+
3+
[README](../../../../README.md)
4+
5+
## Focus
6+
7+
- Passing query parameters with the `params` dict
8+
- Filtering API results by field values
9+
- Paginating results with `_limit` and `_start`
10+
- Understanding the difference between params dict and URL string
11+
12+
## Why this project exists
13+
14+
Most APIs return too much data if you do not tell them what you want. Query parameters are how you filter, sort, and paginate results. This project teaches you two ways to attach parameters to a request and shows you how to work with lists of results instead of a single resource.
15+
16+
## Run
17+
18+
```bash
19+
cd projects/modules/03-rest-apis/02-query-parameters
20+
python project.py
21+
```
22+
23+
## Expected output
24+
25+
```text
26+
--- Method 1: params dict ---
27+
Fetching posts by user 3 (using params dict)...
28+
Found 10 posts by user 3.
29+
Post 21: ea molestias quasi exercitationem repellat qui ipsa sit aut
30+
Post 22: delectus ullam et corporis nulla voluptas sequi
31+
Post 23: nesciunt iure omnis dolorem tempora et accusantium
32+
(showing first 3)
33+
34+
--- Method 2: URL string ---
35+
Fetching posts by user 3 (using URL string)...
36+
Found 10 posts by user 3.
37+
38+
--- Pagination ---
39+
Page 1: fetched 5 posts (IDs: 1, 2, 3, 4, 5)
40+
Page 2: fetched 5 posts (IDs: 6, 7, 8, 9, 10)
41+
Page 3: fetched 5 posts (IDs: 11, 12, 13, 14, 15)
42+
```
43+
44+
## Alter it
45+
46+
1. Change the `userId` filter to a different user (1 through 10 are valid). How many posts does each user have?
47+
2. Change the page size from 5 to 3 and fetch 5 pages instead of 3.
48+
3. Combine filtering and pagination: fetch user 1's posts, 2 at a time.
49+
50+
## Break it
51+
52+
1. Pass `userId=999` — a user that does not exist. What does the API return? Is it an error or an empty list?
53+
2. Set `_limit=0`. What happens?
54+
3. Pass `params={"userId": [1, 2]}` — a list instead of a single value. Does the API handle it? What do you get back?
55+
56+
## Fix it
57+
58+
1. Add a check after each request: if the response is an empty list, print "No results found" instead of trying to iterate.
59+
2. Add validation before the request: if `user_id` is not between 1 and 10, print a warning.
60+
3. After fixing, test with both valid and invalid user IDs.
61+
62+
## Explain it
63+
64+
1. What is the difference between passing `params={"userId": 3}` and putting `?userId=3` directly in the URL string?
65+
2. Why does JSONPlaceholder return an empty list instead of a 404 when you filter by a nonexistent user?
66+
3. How does `_start` differ from `_page` in pagination schemes you might encounter in other APIs?
67+
4. When would you choose params dict over URL string in your own code?
68+
69+
## Mastery check
70+
71+
You can move on when you can:
72+
73+
- pass query parameters using both methods,
74+
- paginate through results using `_start` and `_limit`,
75+
- handle empty results gracefully,
76+
- explain why the params dict is usually preferred.
77+
78+
## Next
79+
80+
Continue to [POST and Auth](../03-post-and-auth/).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Notes — Query Parameters
2+
3+
## What I learned
4+
5+
6+
## What confused me
7+
8+
9+
## What I want to explore next
10+
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Module 03 / Project 02 — Query Parameters.
2+
3+
Learn how to pass query parameters to filter and paginate API results.
4+
Covers two approaches: the params dict and the URL string.
5+
"""
6+
7+
import requests
8+
9+
10+
def fetch_posts_by_user_params_dict(user_id):
11+
"""Fetch all posts by a specific user using the params dict approach.
12+
13+
The params dict is the preferred way to pass query parameters.
14+
requests will URL-encode the values and append them to the URL
15+
automatically: /posts?userId=3
16+
"""
17+
url = "https://jsonplaceholder.typicode.com/posts"
18+
19+
# Pass parameters as a dictionary. requests turns this into
20+
# ?userId=3 appended to the URL. This approach is cleaner and
21+
# handles special characters (spaces, &, =) automatically.
22+
params = {"userId": user_id}
23+
response = requests.get(url, params=params)
24+
posts = response.json()
25+
26+
print("--- Method 1: params dict ---")
27+
print("Fetching posts by user {} (using params dict)...".format(user_id))
28+
print("Found {} posts by user {}.".format(len(posts), user_id))
29+
30+
# Show a preview of the first 3 posts.
31+
for post in posts[:3]:
32+
print(" Post {}: {}".format(post["id"], post["title"]))
33+
if len(posts) > 3:
34+
print(" (showing first 3)")
35+
36+
return posts
37+
38+
39+
def fetch_posts_by_user_url_string(user_id):
40+
"""Fetch all posts by a specific user using a URL string.
41+
42+
This approach puts the query parameters directly in the URL.
43+
It works but is harder to maintain — you have to handle URL encoding
44+
yourself and the URL becomes harder to read with many parameters.
45+
"""
46+
# Here we build the URL string ourselves. For simple cases this is
47+
# fine, but with multiple parameters or special characters, the
48+
# params dict approach is safer.
49+
url = "https://jsonplaceholder.typicode.com/posts?userId={}".format(user_id)
50+
51+
response = requests.get(url)
52+
posts = response.json()
53+
54+
print("\n--- Method 2: URL string ---")
55+
print("Fetching posts by user {} (using URL string)...".format(user_id))
56+
print("Found {} posts by user {}.".format(len(posts), user_id))
57+
58+
return posts
59+
60+
61+
def paginate_posts(page_size, num_pages):
62+
"""Fetch posts in pages using _start and _limit parameters.
63+
64+
JSONPlaceholder supports pagination with:
65+
- _start: the index to start from (0-based)
66+
- _limit: how many items to return
67+
68+
Other APIs might use page/per_page or offset/limit instead.
69+
The concept is the same: fetch a slice of the full dataset.
70+
"""
71+
url = "https://jsonplaceholder.typicode.com/posts"
72+
73+
print("\n--- Pagination ---")
74+
75+
for page_num in range(1, num_pages + 1):
76+
# _start is 0-based. Page 1 starts at 0, page 2 starts at page_size, etc.
77+
start = (page_num - 1) * page_size
78+
79+
params = {
80+
"_start": start,
81+
"_limit": page_size,
82+
}
83+
84+
response = requests.get(url, params=params)
85+
posts = response.json()
86+
87+
# Build a comma-separated string of post IDs for display.
88+
ids = ", ".join(str(post["id"]) for post in posts)
89+
print("Page {}: fetched {} posts (IDs: {})".format(page_num, len(posts), ids))
90+
91+
92+
if __name__ == "__main__":
93+
# Fetch posts by user 3 using both methods.
94+
fetch_posts_by_user_params_dict(3)
95+
fetch_posts_by_user_url_string(3)
96+
97+
# Paginate through posts, 5 per page, 3 pages.
98+
paginate_posts(page_size=5, num_pages=3)

0 commit comments

Comments
 (0)