← Course Home
Module 12

HTTP/API Essentials

Everything in this module is built and graded as local simulations over the real fixture — no live network calls anywhere. Same logic real Okta/Slack/Google scripts use, fully testable offline.

Concept

Recap: Endpoint, Request, Response

You met these in Module 9. An endpoint is a specific URL path an API exposes. A request is what you send; a response is what comes back. This module goes deeper into the parts of a request/response you haven't touched yet: methods, headers, pagination, and rate limits — all as local, no-network simulations.

base_url = "https://example.okta.com"
endpoint = f"{base_url}/api/v1/users"
print(endpoint)
Lab 1

Run the example above. Print the result.

https://example.okta.com/api/v1/users
Lab 2

Build an endpoint string for "https://slack.com" + "/api/chat.postMessage" using an f-string.

https://slack.com/api/chat.postMessage
Lab 3

In one sentence: what's the difference between a request and a response?

request = what you send; response = what comes back
Lab 4

Write a function build_endpoint(base_url, path) that returns the combined URL. Test it with two different base URLs.

Lab 5

Why would a script keep base_url as a separate variable instead of hardcoding the full URL everywhere it's needed?

so the base can change (dev vs prod) without editing every endpoint string
Now go practice

Open problems/01_endpoint_recap.py and do all 4 problems locally.

Concept

HTTP Methods

The method tells the API what KIND of action you want. GET asks for data. POST creates something new. PUT replaces something existing. DELETE removes something.

def build_request(method, endpoint):
    return {"method": method, "endpoint": endpoint}

req = build_request("GET", "/api/v1/users")
print(req)
Lab 1

Run the example above. Print the result.

{'method': 'GET', 'endpoint': '/api/v1/users'}
Lab 2

Which method would you use to fetch a list of users?

GET
Lab 3

Which method would you use to send a new Slack message?

POST
Lab 4

Which method would you use to deactivate (remove) an Okta user?

DELETE
Lab 5

Build 4 request dicts, one for each method (GET/POST/PUT/DELETE), using build_request. Print all 4.

4 dicts, one per method
Now go practice

Open problems/02_http_methods.py and do all 5 problems locally.

Concept

Headers

Headers are metadata sent alongside a request — like auth credentials or the content type. They're just a dict of key/value pairs.

default_headers = {"Content-Type": "application/json"}
custom_headers = {"Authorization": "Bearer abc123"}
merged = {**default_headers, **custom_headers}
print(merged)
Lab 1

Run the example above. Print the merged result.

{'Content-Type': 'application/json', 'Authorization': 'Bearer abc123'}
Lab 2

Write a function build_headers(auth_token) that returns a dict merging the default Content-Type header with an Authorization header built from the token.

Lab 3

Call your build_headers with "xyz789". Print the result.

{'Content-Type': 'application/json', 'Authorization': 'Bearer xyz789'}
Lab 4

If two dicts being merged with {**a, **b} share a key, which one wins — a or b?

b — the later one overwrites
Lab 5

Print just the keys of your headers dict from Lab 3, as a list.

['Content-Type', 'Authorization']
Now go practice

Open problems/03_headers.py and do all 5 problems locally.

Concept

Auth Tokens

An auth token is the credential proving who's calling. It should always come from an env var (Module 10), never hardcoded — then get built into the Authorization header.

import os

def get_required_env(name):
    val = os.environ.get(name)
    if val is None:
        raise ValueError(f"{name} is required but not set")
    return val

def build_auth_header(token):
    return {"Authorization": f"Bearer {token}"}
Lab 1

Write both functions exactly as shown. Call get_required_env on an unset variable name. Let it crash.

CrashesValueError
Lab 2

Set an env var yourself (inside your script, with os.environ["X"] = "..."), then call get_required_env on it. Print the result.

your set value
Lab 3

Call build_auth_header with "abc123". Print the result.

{'Authorization': 'Bearer abc123'}
Lab 4

Combine both: set an env var, read it with get_required_env, then build the auth header from it. Print the header.

Lab 5

In one sentence: why is get_required_env a better choice here than .get() with a default?

a missing token should stop the script loudly, not silently continue with no auth
Now go practice

Open problems/04_auth_tokens.py and do all 5 problems locally.

Concept

Checkpoint: build_request()

Combine method, endpoint, headers, and auth into one function that builds a complete request record.

def build_request(method, base_url, path, token):
    return {
        "method": method,
        "endpoint": f"{base_url}{path}",
        "headers": {"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
    }
Lab 1

Write build_request exactly as shown. Call it with "GET", "https://example.okta.com", "/api/v1/users", "abc123". Print the result.

→ full dict with method/endpoint/headers
Lab 2

Call it again with method "POST". Confirm only the method field changes.

Lab 3

Print just req["headers"]["Authorization"] from your Lab 1 result.

Bearer abc123
Lab 4

Build 3 different requests for 3 different endpoints, same token. Print all 3 endpoint strings.

Lab 5 · Checkpoint

Write a function that builds a request AND validates the method is one of GET/POST/PUT/DELETE, raising ValueError otherwise. Test with a valid and an invalid method.

valid → dict; invalid → crashes
Now go practice

Open problems/05_build_request_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 1-4.

Concept

Status Codes, Deepened

You met 200/401/404 in Module 9. Two more matter a lot in real scripts: 429 means you're calling too fast (rate limited) — back off and retry later. 500 means the SERVER failed — not your fault, but you still need to handle it gracefully.

def handle_status(status_code):
    if status_code == 200:
        return "success"
    elif status_code == 401:
        return "bad auth token"
    elif status_code == 404:
        return "not found"
    elif status_code == 429:
        return "rate limited -- back off"
    elif status_code == 500:
        return "server error -- not your fault"
    else:
        return f"unexpected: {status_code}"
Lab 1

Write handle_status exactly as shown. Call it with all 5 named codes plus one unnamed one (like 418). Print all 6 results.

6 lines
Lab 2

Is a 429 your script's fault, the server's fault, or neither (just a limit being hit)?

neither — you're just calling faster than allowed
Lab 3

Is a 500 something retrying immediately usually fixes?

No — it's a server-side failure, retrying instantly rarely helps
Lab 4

Write a function should_retry(status_code) that returns True for 429 and 500, False otherwise. Test it on all 5 named codes.

False, False, False, True, True
Lab 5

Loop a list of mock status codes [200, 429, 404, 500, 200], printing handle_status's result for each.

5 lines
Now go practice

Open problems/06_status_codes_deepened.py and do all 5 problems locally.

Concept

Mock Response Handling

Since this course hasn't taught classes, a mock "response" is just a plain dict: {"status_code": ..., "json": {...}}. Handle it the same way you'd handle a real one — check the status first, then use the data.

def parse_response(response):
    if response["status_code"] == 200:
        return response["json"]
    raise Exception(f"request failed: {response['status_code']}")

ok_response = {"status_code": 200, "json": {"id": "00u001"}}
bad_response = {"status_code": 404, "json": None}
Lab 1

Write parse_response exactly as shown. Call it on ok_response. Print the result.

{'id': '00u001'}
Lab 2

Call it on bad_response. Let it crash.

CrashesException: request failed: 404
Lab 3

Wrap a call to parse_response(bad_response) in try/except, printing "handled gracefully" instead of crashing.

handled gracefully
Lab 4

Build your own mock response dict for a 500 error. Confirm parse_response raises on it too.

Crashes
Lab 5

Write a function that takes a LIST of mock responses, returning only the .json data from the successful ones (skip failures, don't crash the whole loop).

Now go practice

Open problems/07_mock_response_handling.py and do all 5 problems locally.

Concept

Pagination

Real APIs return results in pages instead of all at once — a "list users" call might return 5 at a time, with a flag telling you whether more pages exist. This mock simulates that over the real fixture, no network needed.

def fetch_page(page_number, page_size=5, users=USERS):
    start = (page_number - 1) * page_size
    end = start + page_size
    return {
        "items": users[start:end],
        "page": page_number,
        "has_more": end < len(users),
    }
Lab 1

Write fetch_page exactly as shown. Call fetch_page(1). Print len(result["items"]) and result["has_more"].

5 then True
Lab 2

Call fetch_page(3) (12 users, page size 5 -- this is the last page). Print the same two values.

2 then False
Lab 3

Call fetch_page(4) (out of range). Print len(result["items"]) -- confirm no crash.

0
Lab 4

Write a loop that calls fetch_page starting at page 1, incrementing until has_more is False, accumulating all items into one list. Print len() of the accumulated list.

12
Lab 5

Call fetch_page(1, page_size=3). Print len(result["items"]).

3
Now go practice

Open problems/08_pagination.py and do all 5 problems locally.

Concept

Rate Limits

A rate limit caps how many calls you can make in a window. This mock uses a plain dict as a call-budget counter — no network, no timers, just a number that goes up and a check against a max.

def call_with_limit(state, max_calls):
    state["calls"] += 1
    if state["calls"] > max_calls:
        raise Exception("429: rate limit exceeded")
    return "ok"
Lab 1

Write call_with_limit exactly as shown. Create state = {"calls": 0}, call it 3 times with max_calls=3. Confirm no crash.

all 3 succeed
Lab 2

Call it a 4th time on the same state. Let it crash.

CrashesException: 429: rate limit exceeded
Lab 3

Fresh state, call call_with_limit 5 times in a loop with max_calls=3, catching the exception each time. Print how many succeeded.

3
Lab 4

Print state["calls"] after that loop -- does it stop incrementing once the limit is hit?

5 — it keeps counting attempts, even failed ones
Lab 5

Write a function calls_remaining(state, max_calls) that returns how many calls are left before hitting the limit (never negative). Test it after 2 calls with max_calls=5.

3
Now go practice

Open problems/09_rate_limits.py and do all 5 problems locally.

Concept

Checkpoint: Paginate Within a Call Budget

Combine pagination and rate limiting: fetch every page, but stop early (gracefully) if the call budget runs out mid-fetch.

Lab 1

Write a function that fetches ALL pages using fetch_page, but calls call_with_limit once per page fetched. Use max_calls=10 (plenty). Print total items fetched.

12
Lab 2

Run the same function with max_calls=2 instead (12 users / page_size 5 = 3 pages needed). Wrap each page-fetch in try/except, stopping the loop (not crashing) when the limit hits.

stops after 2 pages, no crash
Lab 3

Print how many items were fetched before the limit stopped you in Lab 2.

10 (2 pages of 5)
Lab 4

Print a message noting how many pages were successfully fetched vs. how many total pages exist (3).

e.g. fetched 2 of 3 pages
Lab 5 · Checkpoint

Write one function fetch_all_within_budget(max_calls) combining all of the above: paginate, respect the budget, stop gracefully, return whatever items were collected plus a flag saying whether it got everything.

Now go practice

Open problems/10_pagination_rate_limit_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 8-9.

Concept

Mixed Recap: A Realistic Fetch Chain

Now that both halves of this module are taught — request-building (method/endpoint/headers/auth) AND pagination/rate-limits — combine them into one realistic chain, the shape a real Okta/Slack script actually takes.

def mock_api_call(page_number, state, max_calls, token):
    call_with_limit(state, max_calls)
    headers = build_auth_header(token)
    page = fetch_page(page_number)
    return {"status_code": 200, "json": page, "headers_sent": headers}
Lab 1

Write mock_api_call exactly as shown (reuse your earlier functions). Call it for page 1 with a fresh state and max_calls=10. Print result["status_code"].

200
Lab 2

Print result["headers_sent"] from Lab 1.

{'Authorization': 'Bearer ...'}
Lab 3

Call mock_api_call 3 times in a row for pages 1, 2, 3 on the SAME state. Print state["calls"] afterward.

3
Lab 4

Repeat Lab 3 with max_calls=2 instead. Wrap the 3rd call in try/except -- confirm it raises the rate-limit exception.

Crashes on the 3rd call
Lab 5

Combine mock_api_call with parse_response (sub-skill 7): call it, then parse the result to pull out just the page data.

Lab 6

Loop pages 1-3, calling mock_api_call and printing each page's item count.

5, 5, 2
Now go practice

Open problems/11_mixed_recap.py and do all 6 problems locally.

Concept

Final Capstone: The Full Mock API Client

Everything: env-var token → headers → paginated, rate-limited fetch of the whole fixture → status-code handling → flatten (Module 11) → sort/enumerate/write-to-file (Module 10). The heaviest script in the course so far.

Lab 1

Read a token from an env var with get_required_env, build headers from it.

Lab 2

Fetch every page of USERS using your pagination + rate-limit function from sub-skill 10, with a generous max_calls. Confirm all 12 come back.

12
Lab 3

Flatten the fetched users into (user, group) rows, reusing Module 11's flatten_memberships.

15 rows
Lab 4

Handle a MOCK 500 response using handle_status/parse_response-style logic somewhere in your chain -- show it doesn't crash the whole script.

Lab 5

Sort the flattened rows, number them with enumerate, write them to a file (Module 10 style).

a written report file
Lab 6

Print a one-line summary before writing: total rows, distinct groups, and the env-var-derived token's first 4 characters (never print a full token in real code).

Lab 7 · Final checkpoint

One script, start to finish: auth → paginated/rate-limited fetch → flatten → sort/number → write to file → summary printed. Zero crashes, end to end.

This is the biggest script in the course so far. Take your time, then explain it in three sentences, interview-style.
Now go practice

Open problems/12_final_capstone.py — 7 problems, #7 is the final checkpoint for all of Module 12.

Complete

Module 12 done

endpoint/request/response recap → HTTP methods → headers → auth tokens → build_request() → status codes deepened (429/500) → mock response handling → pagination → rate limits → paginate within a budget → a realistic mixed fetch chain → the full mock API client, tying together everything from Modules 10-12.

Run python check.py in problems/ to grade everything.