Contents

Week 9: How the Web Works, requests, and an Introduction to FastAPI

Month 3 | Main goal: The student understands client/server and HTTP, uses requests to talk to a public API, then builds their first API with FastAPI.


Teacher Preparation (read before the week starts)

  1. Read:
  2. Try it yourself before class — write ALL the code from Sessions 1–3 on your teaching computer (the one you share your screen from). Do not go into class without having seen the output with your own eyes.
  3. Prepare a folder on the teaching computer:

    powershell
    cd $HOME\Documents
    mkdir wiki09
    cd wiki09
    python -m venv .venv
    .\.venv\Scripts\Activate.ps1
    pip install requests "fastapi[standard]"
    

    If you get the error running scripts is disabled on this system, run this once: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned then answer Y.

  4. Make sure the internet is working and that you can open https://jsonplaceholder.typicode.com/todos/1 in a browser. This is the "public API" we will use — it is free and needs no account or key.

  5. VS Code extensions: Python (Microsoft). Optional: "Thunder Client" or "REST Client" — but this week we will use the browser, FastAPI's /docs, and requests, so they are not needed.
  6. Key terms to remember (write them on a sheet of paper beside you): client, server, request, response, URL, endpoint, path, query string, method (GET/POST/PUT/PATCH/DELETE), status code, header, body, JSON, API.

Week Goals (by the end of the week the student will be able to...)

  1. Explain in their own words what a client and a server are, and what a request and a response are.
  2. Break a URL into its parts: scheme, host, port, path, query string.
  3. Name the 5 main HTTP methods and what each does (GET, POST, PUT, PATCH, DELETE).
  4. Recognise the status code groups (2xx, 3xx, 4xx, 5xx) and the important codes: 200, 201, 204, 400, 401, 403, 404, 422, 500.
  5. Use requests to do GET and POST, send query parameters, read JSON, and handle errors (timeouts, bad status).
  6. Install FastAPI and run a server with fastapi dev main.py.
  7. Write endpoints with path parameters and query parameters that have type hints.
  8. Use /docs (Swagger UI) to try out their API.

Session 1 — How the Web Works: Client, Server, HTTP

Session flow

  1. Review of the Week 8 homework (Git/GitHub) — the student shows their repo
  2. The big picture: client and server, request and response (the restaurant analogy)
  3. How a URL is built: scheme, host, port, path, query
  4. HTTP methods and status codes
  5. What JSON is and why APIs use it; opening an API in the browser
  6. Browser DevTools: seeing real requests (Network tab)
  7. In-class exercise
  8. Q&A + homework

Explanation for the teacher (read this first)

Client and server. Two computers (or two programs) are talking. One asks for something — that is the client (for example a browser, a phone app, a Python script). The other waits for requests and answers them — that is the server (a program running somewhere, listening on a "port").

Request and response. All web conversation comes in pairs: the client sends a request, the server returns a response. There is no response without a request. With ordinary HTTP, the server cannot "call" the client on its own.

HTTP is the "language" (protocol) they use. An HTTP request has these parts:

  • Method — the action: GET (read), POST (create something new), PUT (replace the whole thing), PATCH (change part of it), DELETE (delete).
  • URL — the address of the thing you want.
  • Headers — extra information (for example Content-Type: application/json, Authorization: Bearer ...).
  • Body — the data you are sending (usually on POST/PUT/PATCH). GET normally has no body.

A response has:

  • Status code — a number giving the result (200 = OK, 404 = not found...).
  • Headers — for example Content-Type: application/json.
  • Body — the data itself (HTML for a web page, JSON for an API).

URL example: https://jsonplaceholder.typicode.com:443/posts?userId=1&_limit=5

  • https — scheme (http has no encryption; https is secure/encrypted)
  • jsonplaceholder.typicode.com — host (the server's name)
  • 443 — port (for https it is 443 by default, which is why it is usually not written; http is 80; our FastAPI server will be 8000)
  • /posts — path (which resource/thing)
  • ?userId=1&_limit=5 — query string (filters, key=value pairs separated by &)

Endpoint = the combination of method + path, for example GET /posts or POST /posts. These are two DIFFERENT endpoints even though the path is the same.

API (Application Programming Interface) — on the web, it is a set of endpoints that other programs can call, and they usually return JSON rather than HTML. A website is for humans; an API is for programs.

JSON — the student already knows it from the files week (the json module). Here, emphasise: JSON is text that looks like a Python dict/list. APIs use it because every language (JavaScript, Python, Java...) can read it.

Status codes — groups:

  • 1xx — informational (we rarely use these)
  • 2xx — success: 200 OK, 201 Created, 204 No Content
  • 3xx — redirect (go somewhere else): 301, 302, 304
  • 4xx — CLIENT error (you made the mistake): 400 Bad Request, 401 Unauthorized (not logged in), 403 Forbidden (logged in but no permission), 404 Not Found, 405 Method Not Allowed, 422 Unprocessable Entity (the data is not right — FastAPI uses this a lot)
  • 5xx — SERVER error: 500 Internal Server Error, 503 Service Unavailable

A simple rule: 4 = "your fault", 5 = "my fault (the server's)".

Stateless. HTTP does not remember the previous request. Each request stands on its own. That is why later (Week 11) we will send a "token" with every request so the server knows who we are.

Say this to the student

"Imagine you are in a restaurant. You are the client. The kitchen is the server. The waiter is HTTP — the one who carries the messages. You order: 'I'd like chipsi mayai' — that is the request. The waiter brings the plate — that is the response. If the chipsi have run out, the waiter says 'Sorry, there are none' — that is 404 Not Found. If the kitchen catches fire, that is 500 — the fault isn't yours, it's the kitchen's."

"The restaurant's menu is the API — it tells you what you can order and how to order it. You can't walk into the kitchen yourself; you go through the menu and the waiter."

"Methods are verbs: GET = 'give me/show me', POST = 'create/add', PUT = 'replace all of it', PATCH = 'fix a little', DELETE = 'delete'. If we have a student register: reading the list is GET, enrolling a new student is POST, changing only a phone number is PATCH, removing a student is DELETE."

"A URL is like a postal address: https is the type of transport (a vehicle with a lock), the host is the name of the building, the port is the door number, the path is which room, and the query string is extra detail — 'give me only the files from 2026'."

Type this code live (share screen)

Step 1 — Open an API in the browser. Type into the browser:

text
https://jsonplaceholder.typicode.com/todos/1

Expected output (the browser shows JSON):

json
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

Say: "This is an API response. It isn't a nice HTML page — it is plain data, meant for programs."

Also try:

  • https://jsonplaceholder.typicode.com/users/1 — one user
  • https://jsonplaceholder.typicode.com/posts?userId=1 — posts from user 1 only (query string!)
  • https://jsonplaceholder.typicode.com/todos/99999 — you get {} and status 404

Step 2 — DevTools. Press F12 in Chrome/Edge → Network tab → refresh (F5). Click the 1 request (todos/1). Show the student:

  • Headers → Request URL, Request Method: GET, Status Code: 200
  • Response Headers → content-type: application/json; charset=utf-8
  • Response → the JSON itself

Then open todos/99999 and show Status Code: 404.

Step 3 — A request from PowerShell (to show that a browser isn't required). In the VS Code terminal:

powershell
Invoke-RestMethod https://jsonplaceholder.typicode.com/todos/1

Output:

text
userId id title              completed
------ -- -----              ---------
     1  1 delectus aut autem     False

Or with curl.exe (remember the .exe — in Windows PowerShell 5.1, curl alone is an alias for Invoke-WebRequest):

powershell
curl.exe -i https://jsonplaceholder.typicode.com/todos/1

-i shows the headers. Show the student the first line: HTTP/1.1 200 OK (or HTTP/2 200).

Step 4 — JSON and Python (review). Create json_marudio.py:

python
import json

maandishi = '{"jina": "Amina", "umri": 21, "masomo": ["Python", "Git"], "ameandikishwa": true}'

data = json.loads(maandishi)       # text -> dict
print(type(data))                  # <class 'dict'>
print(data["masomo"][0])           # Python
print(data["ameandikishwa"])       # True  (JSON true -> Python True)

nyuma = json.dumps(data, ensure_ascii=False)  # dict -> text
print(type(nyuma))                 # <class 'str'>
print(nyuma)

Output:

text
<class 'dict'>
Python
True
<class 'str'>
{"jina": "Amina", "umri": 21, "masomo": ["Python", "Git"], "ameandikishwa": true}

Emphasise the difference: JSON true/false/null ↔ Python True/False/None. JSON uses only double quotes ".

Step 5 — Breaking up a URL with Python (shows the parts of a URL clearly):

python
from urllib.parse import urlparse, parse_qs

url = "https://jsonplaceholder.typicode.com:443/posts?userId=1&_limit=5"
sehemu = urlparse(url)

print("scheme:", sehemu.scheme)
print("host:  ", sehemu.hostname)
print("port:  ", sehemu.port)
print("path:  ", sehemu.path)
print("query: ", sehemu.query)
print("query kama dict:", parse_qs(sehemu.query))

Output:

text
scheme: https
host:   jsonplaceholder.typicode.com
port:   443
path:   /posts
query:  userId=1&_limit=5
query kama dict: {'userId': ['1'], '_limit': ['5']}

Notice: query values are always text ('1', not 1). FastAPI will convert them to int for us later using type hints.

Questions to ask the student

  1. Is your browser a client or a server? — Client.
  2. When you open YouTube on your phone, who is the client and who is the server? — The YouTube app on the phone is the client; Google's computers are the server.
  3. Are GET /posts and POST /posts one endpoint? — No, they are two different ones; the method is part of the endpoint.
  4. What does status 404 mean, and whose fault is it? — Not found; the 4xx group is a client error (you asked for something that doesn't exist).
  5. What is the difference between 401 and 403? — 401: the server doesn't know you (you aren't logged in). 403: it knows you but you don't have permission.
  6. In https://shop.co.tz/bidhaa?aina=simu&bei_max=500000, what is the path and what is the query? — Path: /bidhaa. Query: aina=simu&bei_max=500000.
  7. Why do APIs use JSON rather than HTML? — JSON is plain structured data, easy for any program (any language) to read; HTML is for displaying to humans.
  8. If you want to change only a customer's phone number, which method would you use? — PATCH (or PUT if you send the whole customer).
  9. What does it mean that HTTP is "stateless"? — Each request stands on its own; the server does not by itself remember the previous request.

Common mistakes

Mistake Message / symptom Solution
Using curl instead of curl.exe in Windows PowerShell 5.1 Cannot process command because of one or more missing mandatory parameters: Uri. (or PowerShell prompts for Uri) Type curl.exe
Mixing up / and ? The URL .../posts/userId=1 returns 404 A query starts with ?: /posts?userId=1
Separating query parameters with , or a second ? ?userId=1?_limit=5 doesn't work properly Use &: ?userId=1&_limit=5
JSON with single quotes json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes JSON accepts only "
Writing True inside JSON text JSONDecodeError: Expecting value Inside JSON use true, false, null
Assuming query values are numbers parse_qs returns ['1'] Convert with int(...) (FastAPI does this on its own)

In-class exercise

Exercise 1.1 — For each URL, name the scheme, host, path, and query (out loud or in writing):

  • a) http://127.0.0.1:8000/items/5?q=simu
  • b) https://api.github.com/users/octocat/repos?per_page=3

Exercise 1.2 — Match each action to the right method + status code:

  • a) Requesting a list of products → ?
  • b) Successfully creating a new account → ?
  • c) Requesting a product whose id doesn't exist → ?
  • d) Successfully deleting a product without returning data → ?
  • e) Submitting a form with age = "kumi" (ten, as a word) instead of a number → ?

Exercise 1.3 — Open https://jsonplaceholder.typicode.com/comments?postId=1 in the browser. Use DevTools (Network) to report: the status code, the content-type, and how many comments came back (count them in the JSON).

Exercise 1.4 — Write a script url_vunja.py that asks the user for a URL with input(), then prints the host, the path, and each query parameter on its own line.

Answer

1.1

  • a) scheme http, host 127.0.0.1 (port 8000), path /items/5, query q=simu
  • b) scheme https, host api.github.com, path /users/octocat/repos, query per_page=3

1.2

  • a) GET → 200 OK
  • b) POST → 201 Created
  • c) GET → 404 Not Found
  • d) DELETE → 204 No Content
  • e) POST → 422 Unprocessable Entity (or 400 Bad Request)

1.3 Status 200, content-type: application/json; charset=utf-8, 5 comments.

1.4

python
from urllib.parse import urlparse, parse_qs

url = input("Weka URL: ")
sehemu = urlparse(url)

print("Host:", sehemu.hostname)
print("Path:", sehemu.path)

query = parse_qs(sehemu.query)
if not query:
    print("Hakuna query parameters.")
for key, values in query.items():
    print(f"  {key} = {values[0]}")

Example:

text
Weka URL: https://shop.co.tz/bidhaa?aina=simu&bei_max=500000
Host: shop.co.tz
Path: /bidhaa
  aina = simu
  bei_max = 500000

Session 2 — Talking to an API with requests

Session flow

  1. Homework review (HTTP questions / the URL script)
  2. Installing requests inside the venv; why not urllib
  3. requests.get: status_code, headers, .json(), params=
  4. requests.post with json=; reading a 201
  5. Errors: timeout, raise_for_status(), ConnectionError
  6. Mini-project: download todos, filter them, save to a JSON file
  7. In-class exercise
  8. Q&A + homework

Explanation for the teacher (read this first)

requests is a popular Python library for sending HTTP requests. Python has urllib built in, but requests is much easier to read and write.

Important things to know:

  • requests.get(url) returns a Response object.
  • response.status_code → a number (for example 200).
  • response.ok → True if the status is below 400.
  • response.headers → a dict-like object of headers.
  • response.text → the body as a string.
  • response.json() → the body converted to a Python dict/list (it does json.loads for you). If the body isn't JSON, it raises an error.
  • params={"userId": 1} → requests builds the query string for you (?userId=1). Don't join strings yourself.
  • json={"title": "..."} on a POST → requests converts the dict to JSON and sets the header Content-Type: application/json.
  • timeout=10 → don't wait forever. Always set a timeout. Without one, a script can hang.
  • response.raise_for_status() → if the status is 4xx or 5xx, it raises requests.HTTPError. If all is well, it does nothing.
  • Important exceptions (all children of requests.RequestException): requests.ConnectionError (no internet / server not found), requests.Timeout, requests.HTTPError.

Note: JSONPlaceholder is a "fake" API — POST returns 201 and an object with id: 101, but it doesn't really save anything. Explain this to the student so they aren't surprised when GET /posts/101 returns 404.

Say this to the student

"Last session we used the browser as the client. Today our Python will be the client. requests is like giving your script a phone to call the server with."

"params= is like telling the waiter 'chipsi, but without chilli' — those are extra details (the query). json= is like handing the waiter a written letter — that is the body."

"timeout is your patience. If you call and nobody picks up within 10 seconds, you hang up. Without a timeout, the script will wait forever, like someone who forgot the phone at their ear."

"raise_for_status() is like a guard: if the answer is bad (4xx/5xx), it shouts (an exception) immediately instead of letting you carry on with bad data."

Type this code live (share screen)

Step 1 — Install. In the terminal (the venv must be active — look for (.venv) at the start of the line):

powershell
pip install requests
pip show requests

Step 2 — The first GET. Create api_1.py:

python
import requests

url = "https://jsonplaceholder.typicode.com/todos/1"
response = requests.get(url, timeout=10)

print("Status:", response.status_code)
print("OK?:", response.ok)
print("Content-Type:", response.headers["Content-Type"])
print("Text:", response.text[:60], "...")

data = response.json()
print(type(data))
print("Title:", data["title"])
print("Imekamilika?:", data["completed"])

Output:

text
Status: 200
OK?: True
Content-Type: application/json; charset=utf-8
Text: {
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem", ...
<class 'dict'>
Title: delectus aut autem
Imekamilika?: False

Step 3 — A list and params. Create api_2.py:

python
import requests

BASE = "https://jsonplaceholder.typicode.com"

response = requests.get(f"{BASE}/todos", params={"userId": 1}, timeout=10)
print("URL iliyotumwa:", response.url)

todos = response.json()
print(type(todos), "idadi:", len(todos))

zilizokamilika = [t for t in todos if t["completed"]]
print("Zilizokamilika:", len(zilizokamilika))

for t in todos[:3]:
    alama = "✔" if t["completed"] else "✘"
    print(f"{alama} {t['id']}: {t['title']}")

Output:

text
URL iliyotumwa: https://jsonplaceholder.typicode.com/todos?userId=1
<class 'list'> idadi: 20
Zilizokamilika: 11
✘ 1: delectus aut autem
✘ 2: quis ut nam facilis et officia qui
✘ 3: fugiat veniam minus

Emphasise: response.url shows that requests built ?userId=1 by itself.

If the terminal shows strange symbols instead of ✔/✘, change them to "[x]" and "[ ]". (Modern Windows Terminal displays them fine.)

Step 4 — POST. Create api_3.py:

python
import requests

post_mpya = {
    "title": "Habari kutoka Dar",
    "body": "Najifunza requests leo.",
    "userId": 1,
}

response = requests.post(
    "https://jsonplaceholder.typicode.com/posts",
    json=post_mpya,
    timeout=10,
)

print("Status:", response.status_code)
print(response.json())

Output:

text
Status: 201
{'title': 'Habari kutoka Dar', 'body': 'Najifunza requests leo.', 'userId': 1, 'id': 101}

Explain: 201 Created — the server is saying "I created it". id: 101 is the new id given by the server (fake here).

Step 5 — Handling errors. Create api_4.py:

python
import requests

def pata_user(user_id: int) -> dict | None:
    url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
    except requests.HTTPError as e:
        print(f"Kosa la HTTP: {e.response.status_code}")
        return None
    except requests.ConnectionError:
        print("Hakuna mtandao au server haipatikani.")
        return None
    except requests.Timeout:
        print("Server imechelewa sana.")
        return None
    return response.json()


user = pata_user(1)
if user:
    print(user["name"], "-", user["email"], "-", user["address"]["city"])

pata_user(999)

Output:

text
Leanne Graham - Sincere@april.biz - Gwenborough
Kosa la HTTP: 404

Then turn off Wi-Fi (or change the URL to https://haipo-kabisa-12345.com) and run it again — show Hakuna mtandao au server haipatikani. (no internet or the server can't be reached). This makes the lesson stick.

Step 6 — Mini-project: download and save. Create hifadhi_todos.py:

python
import json
from pathlib import Path

import requests

BASE = "https://jsonplaceholder.typicode.com"


def pakua_todos(user_id: int) -> list[dict]:
    r = requests.get(f"{BASE}/todos", params={"userId": user_id}, timeout=10)
    r.raise_for_status()
    return r.json()


def main() -> None:
    todos = pakua_todos(2)
    hazijakamilika = [
        {"id": t["id"], "title": t["title"]}
        for t in todos
        if not t["completed"]
    ]
    faili = Path("todos_user2.json")
    faili.write_text(json.dumps(hazijakamilika, indent=2), encoding="utf-8")
    print(f"Nimehifadhi todos {len(hazijakamilika)} kwenye {faili}")


if __name__ == "__main__":
    main()

Output:

text
Nimehifadhi todos 12 kwenye todos_user2.json

Open todos_user2.json in VS Code and show the student.

Questions to ask the student

  1. What does response.json() return for /todos/1 and for /todos? — The first a dict, the second a list (of dicts).
  2. Why do we use params={...} rather than writing ?userId=1 in the string? — Requests builds and encodes the query properly (spaces, special characters), and the code is more readable.
  3. What is the difference between json= and params=? — params goes in the URL (query string); json goes in the body.
  4. What is the danger of not setting a timeout? — The script can wait forever if the server doesn't answer.
  5. What does raise_for_status() do on a status 200? — Nothing; it raises an error only for 4xx/5xx.
  6. Why is GET /posts/101 a 404 after a successful POST? — JSONPlaceholder is a fake API; it doesn't save data.
  7. If requests.get(...) completes but the status is 404, is an exception raised? — No! Not unless you call raise_for_status(). That is why we call it.

Common mistakes

Mistake Message / symptom Solution
requests not installed or venv not active ModuleNotFoundError: No module named 'requests' Activate the venv (.\.venv\Scripts\Activate.ps1), pip install requests; VS Code: choose the .venv interpreter (Ctrl+Shift+P → "Python: Select Interpreter")
Naming a file requests.py AttributeError: partially initialized module 'requests' has no attribute 'get' (most likely due to a circular import) Rename the file, delete the __pycache__ folder
Forgetting the () on .json You get <bound method Response.json of <Response [200]>> response.json()
Calling .json() on a non-JSON response requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0) Check status_code and headers["Content-Type"] first
Writing data= instead of json= The server receives form data, not JSON; many APIs return 400/422 Use json=dict for JSON APIs
t.completed instead of t["completed"] AttributeError: 'dict' object has no attribute 'completed' JSON → dict; use brackets [...]
Putting timeout in a string ValueError timeout=10 (a number)

In-class exercise

Exercise 2.1 — Write a script that downloads https://jsonplaceholder.typicode.com/users and prints each user's name and city (address.city).

Exercise 2.2 — Write a function posts_za(user_id: int) -> int (posts of) that returns how many posts that user has (use params). Test it for users 1 and 5.

Exercise 2.3 — Write a function tengeneza_todo(title: str) -> dict (create todo) that sends POST /todos with {"title": title, "completed": False, "userId": 1} and returns the JSON of the response. Make sure the status is 201 using raise_for_status() and assert.

Exercise 2.4 (challenge) — Work out which user (userId) has the most completed todos. Use only ONE request (/todos).

Answer

2.1

python
import requests

r = requests.get("https://jsonplaceholder.typicode.com/users", timeout=10)
r.raise_for_status()
for u in r.json():
    print(f"{u['name']:<25} {u['address']['city']}")

Output (10 lines; the beginning):

text
Leanne Graham             Gwenborough
Ervin Howell              Wisokyburgh
Clementine Bauch          McKenziehaven
...

2.2

python
import requests

def posts_za(user_id: int) -> int:
    r = requests.get(
        "https://jsonplaceholder.typicode.com/posts",
        params={"userId": user_id},
        timeout=10,
    )
    r.raise_for_status()
    return len(r.json())

print(posts_za(1))  # 10
print(posts_za(5))  # 10

2.3

python
import requests

def tengeneza_todo(title: str) -> dict:
    r = requests.post(
        "https://jsonplaceholder.typicode.com/todos",
        json={"title": title, "completed": False, "userId": 1},
        timeout=10,
    )
    r.raise_for_status()
    assert r.status_code == 201, r.status_code
    return r.json()

print(tengeneza_todo("Kununua sukari"))
# {'title': 'Kununua sukari', 'completed': False, 'userId': 1, 'id': 201}

2.4

python
from collections import Counter
import requests

r = requests.get("https://jsonplaceholder.typicode.com/todos", timeout=10)
r.raise_for_status()

hesabu = Counter(t["userId"] for t in r.json() if t["completed"])
user_id, idadi = hesabu.most_common(1)[0]
print(f"User {user_id} ana todos {idadi} zilizokamilika")
print(hesabu)

Output (first line): User 5 ana todos 12 zilizokamilika (User 5 has 12 completed todos). Users 5 and 10 are tied at 12; most_common returns the one seen first.


Session 3 — FastAPI: Your First API

Session flow

  1. Homework review (the requests scripts)
  2. What FastAPI is; now WE are the server
  3. Installing, the first main.py, fastapi dev main.py
  4. /docs and /redoc — free documentation
  5. Path parameters with types; 422 if the type is wrong
  6. Query parameters: default, optional (None), bool
  7. Calling our own API with requests
  8. In-class exercise
  9. Q&A + homework

Explanation for the teacher (read this first)

FastAPI is a Python framework for building APIs. It uses type hints (which the student knows from Week 7) to do three things for us:

  1. Data conversion — "5" from the URL becomes 5 (int).
  2. Data validation — "abc" for an int returns a 422 with details.
  3. Documentation — /docs builds itself.

Uvicorn is the server (an ASGI server) that runs the FastAPI app. pip install "fastapi[standard]" installs FastAPI together with uvicorn, the fastapi CLI, httpx, python-multipart, and more. The quotation marks "..." matter in PowerShell because of the square brackets [ ].

fastapi dev main.py — runs the server in "development mode": it restarts itself (auto-reload) every time you save a file. It listens on http://127.0.0.1:8000. For production there is fastapi run main.py (we won't use it now). To stop the server: Ctrl+C in the terminal.

127.0.0.1 (or localhost) = "this very computer". So the client (browser) and the server (FastAPI) are on the same computer.

Decorator @app.get("/path") — the student learned decorators in Week 8. Here the decorator "registers" a function: "when a GET request arrives at /path, call this function". The function returns a dict/list → FastAPI converts it to JSON.

Path parameter: a variable part of the path, written {jina} (name), and the function receives an argument with that same name: @app.get("/items/{item_id}") → def read_item(item_id: int).

Query parameter: any function argument that is NOT in the path becomes a query parameter. With a default → optional. Without a default → required (leaving it out gives a 422). q: str | None = None → optional, default None.

Bool in a query: FastAPI accepts true/false, 1/0, yes/no, on/off.

Route order matters: FastAPI checks routes in the order they are written. /users/me must be written BEFORE /users/{user_id}, otherwise "me" will be tried as a user_id and give a 422.

def or async def? Both work. In this course we will use plain def — it is simple and correct. async def is for "await" work (we will only mention it). If asked: "FastAPI runs def in a threadpool, so it doesn't block the server."

Say this to the student

"Until now we have been customers in the restaurant. Today we open our own restaurant! FastAPI is our kitchen, and every @app.get(...) is a new dish on the menu."

"The type hints you learned earlier were 'just notes' — Python didn't care about them. FastAPI takes them seriously: if you say item_id: int, FastAPI will reject 'abc' for you. It is like a guard at the gate checking IDs."

"/docs is like a menu printed for free — you wrote the code, and the menu made itself, and you can even order straight from the menu (Try it out)."

"A path parameter is 'who/which' — /wanafunzi/7 = student number 7. A query parameter is 'how' — /wanafunzi?darasa=4&limit=10 = show me those in class 4, only ten."

Type this code live (share screen)

Step 1 — Folder and installation.

powershell
cd $HOME\Documents
mkdir api_ya_kwanza
cd api_ya_kwanza
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install "fastapi[standard]"
code .

Step 2 — The first main.py:

python
from fastapi import FastAPI

app = FastAPI(title="API ya Kwanza")


@app.get("/")
def mwanzo():
    return {"ujumbe": "Karibu kwenye API yangu!"}

Run:

powershell
fastapi dev main.py

Output (the important part):

text
 ⚡️ Starting FastAPI in development mode

 🐍 Using import string: main:app

 🌐 Server started at http://127.0.0.1:8000
    Documentation at http://127.0.0.1:8000/docs

  Logs:

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Application startup complete.

(The layout and decoration of this text changes between FastAPI versions. What matters is the server address and Application startup complete.)

Open http://127.0.0.1:8000 in the browser:

json
{"ujumbe":"Karibu kwenye API yangu!"}

Look at the terminal — there is a line like INFO: 127.0.0.1:52344 - "GET / HTTP/1.1" 200 OK. Say: "That is the browser's request arriving at our server. We are the server now!"

Step 3 — /docs. Open http://127.0.0.1:8000/docs. Click GET / → "Try it out" → "Execute". Show the Response body and status 200. Then http://127.0.0.1:8000/redoc (another style). Also http://127.0.0.1:8000/openapi.json — "this is the description of the API for machines; /docs is built from it".

Step 4 — Data and path parameters. Change main.py (the server restarts itself when you save):

python
from fastapi import FastAPI

app = FastAPI(title="API ya Kwanza")

BIDHAA = {
    1: {"jina": "Sukari", "bei": 3000, "aina": "chakula"},
    2: {"jina": "Mchele", "bei": 2800, "aina": "chakula"},
    3: {"jina": "Sabuni", "bei": 1500, "aina": "usafi"},
    4: {"jina": "Daftari", "bei": 1000, "aina": "shule"},
    5: {"jina": "Kalamu", "bei": 500, "aina": "shule"},
}


@app.get("/")
def mwanzo():
    return {"ujumbe": "Karibu kwenye API yangu!"}


@app.get("/bidhaa/{bidhaa_id}")
def pata_bidhaa(bidhaa_id: int):
    if bidhaa_id not in BIDHAA:
        return {"kosa": "Bidhaa haipo"}
    return {"id": bidhaa_id, **BIDHAA[bidhaa_id]}

Try in the browser:

  • http://127.0.0.1:8000/bidhaa/3 → {"id":3,"jina":"Sabuni","bei":1500,"aina":"usafi"}
  • http://127.0.0.1:8000/bidhaa/abc → 422:
json
{"detail":[{"type":"int_parsing","loc":["path","bidhaa_id"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"abc"}]}

Say: "We didn't write any if to check for 'abc'. FastAPI did it on its own because we wrote int."

  • http://127.0.0.1:8000/bidhaa/99 → {"kosa":"Bidhaa haipo"} — but the status is 200! Show the student in DevTools or /docs. Say: "This isn't right at all — we should return 404. Next week we will learn HTTPException to fix this." (This is a nice hook into Week 10.)

Step 5 — Query parameters. Add:

python
@app.get("/bidhaa")
def orodha_bidhaa(aina: str | None = None, bei_max: int | None = None, limit: int = 10):
    matokeo = []
    for bid, b in BIDHAA.items():
        if aina is not None and b["aina"] != aina:
            continue
        if bei_max is not None and b["bei"] > bei_max:
            continue
        matokeo.append({"id": bid, **b})
    return matokeo[:limit]

Try:

  • /bidhaa → all 5 products
  • /bidhaa?aina=shule → Daftari (exercise book) and Kalamu (pen)
  • /bidhaa?bei_max=2000&limit=2 → Sabuni (soap) and Daftari
  • /bidhaa?limit=kumi → 422 (int_parsing, loc: ["query","limit"])

Open /docs — show that aina, bei_max, limit appear as query parameters, and limit has a default of 10.

Step 6 — Route order (a trap). Add BELOW /bidhaa/{bidhaa_id}:

python
@app.get("/bidhaa/nafuu")
def bidhaa_nafuu():
    return [b for b in BIDHAA.values() if b["bei"] <= 1000]

Open /bidhaa/nafuu → 422! Ask: "Why?" Answer: the /bidhaa/{bidhaa_id} route is written above, so "nafuu" (cheap) is tried as bidhaa_id: int. Move bidhaa_nafuu above pata_bidhaa → now it works:

json
[{"jina":"Daftari","bei":1000,"aina":"shule"},{"jina":"Kalamu","bei":500,"aina":"shule"}]

Step 7 — Required query and bool:

python
@app.get("/salamu")
def salamu(jina: str, rasmi: bool = False):
    if rasmi:
        return {"salamu": f"Shikamoo, {jina}."}
    return {"salamu": f"Mambo {jina}!"}
  • /salamu?jina=Juma → {"salamu":"Mambo Juma!"}
  • /salamu?jina=Bibi&rasmi=true → {"salamu":"Shikamoo, Bibi."}
  • /salamu → 422 "type":"missing","loc":["query","jina"],"msg":"Field required"

Step 8 — Calling our API with requests. With the server still running, open a second terminal (the + button in the VS Code terminal), activate the venv, create mteja.py:

python
import requests

BASE = "http://127.0.0.1:8000"

r = requests.get(f"{BASE}/bidhaa", params={"aina": "chakula"}, timeout=5)
print(r.status_code, r.json())

r = requests.get(f"{BASE}/salamu", params={"jina": "Neema", "rasmi": True}, timeout=5)
print(r.json()["salamu"])

Output:

text
200 [{'id': 1, 'jina': 'Sukari', 'bei': 3000, 'aina': 'chakula'}, {'id': 2, 'jina': 'Mchele', 'bei': 2800, 'aina': 'chakula'}]
Shikamoo, Neema.

Say: "Now you have connected the whole week: Session 1 (HTTP), Session 2 (client), Session 3 (server)."

Questions to ask the student

  1. Why do we write "fastapi[standard]" in quotes? — PowerShell may interpret the brackets [ ]; the quotes make sure pip receives the whole name. [standard] brings in uvicorn and other tools.
  2. What does fastapi dev do when you save a file? — It restarts (reloads) itself.
  3. If you write def pata(bidhaa_id: int) and the path is /bidhaa/{id}, what happens? — The names don't match; FastAPI treats bidhaa_id as a required query parameter, and the path's id goes unused → 422 if you leave out ?bidhaa_id=. The names must match.
  4. When is a query parameter required? — When it has no default.
  5. Why did /bidhaa/nafuu give a 422 at first? — Route order: {bidhaa_id} caught "nafuu" first.
  6. The function returns a dict; what does the client receive? — JSON (FastAPI converts it).
  7. What status did /bidhaa/99 return, and is it correct? — 200; it isn't correct, it should be 404 (we will fix it in Week 10).
  8. What is 127.0.0.1? — Your own computer (localhost).

Common mistakes

Mistake Message / symptom Solution
fastapi command not found fastapi : The term 'fastapi' is not recognized... The venv isn't active or it wasn't installed: activate, pip install "fastapi[standard]"
Installing fastapi without [standard] The fastapi command is missing, or a message saying To use the fastapi command, please install "fastapi[standard]" pip install "fastapi[standard]"
Port 8000 already in use (another server is still running) ERROR: [Errno 10048] error while attempting to bind on address ('127.0.0.1', 8000) Stop the other server (Ctrl+C in its terminal) or fastapi dev main.py --port 8001
Not having app = FastAPI() or giving it another name Could not find FastAPI app in module, try using --app Name the variable app, or fastapi dev main.py --app jina_lako
Path param and argument names differ 422 "loc":["query","bidhaa_id"],"msg":"Field required" {bidhaa_id} ↔ bidhaa_id: int
Route order 422 on /bidhaa/nafuu Put routes with specific paths above those with a {param}
Opening https://127.0.0.1:8000 Browser: "This site can't provide a secure connection" Use http:// (not https) locally
Running mteja.py while the server is stopped requests.exceptions.ConnectionError: ... [WinError 10061] No connection could be made because the target machine actively refused it Start the server first in another terminal

In-class exercise

Exercise 3.1 — Add an endpoint GET /kuhusu (about) that returns {"jina": "<jina lako>", "kozi": "Python", "wiki": 9} (with your own name in jina).

Exercise 3.2 — Add an endpoint GET /hesabu/jumla/{a}/{b} that returns {"a": a, "b": b, "jumla": a + b}. a and b should be float. Try /hesabu/jumla/2.5/4.

Exercise 3.3 — Add an endpoint GET /bidhaa/tafuta (search products) with a required query parameter neno: str (word), which returns products whose name contains that word (ignoring upper/lower case). Example: /bidhaa/tafuta?neno=SU → Sukari (sugar). (Remember route order!)

Exercise 3.4 — Add a query parameter panga: bool = False (sort) to GET /bidhaa; if true, sort the results by price (lowest first).

Answer
python
from fastapi import FastAPI

app = FastAPI(title="API ya Kwanza")

BIDHAA = {
    1: {"jina": "Sukari", "bei": 3000, "aina": "chakula"},
    2: {"jina": "Mchele", "bei": 2800, "aina": "chakula"},
    3: {"jina": "Sabuni", "bei": 1500, "aina": "usafi"},
    4: {"jina": "Daftari", "bei": 1000, "aina": "shule"},
    5: {"jina": "Kalamu", "bei": 500, "aina": "shule"},
}


@app.get("/")
def mwanzo():
    return {"ujumbe": "Karibu kwenye API yangu!"}


# 3.1
@app.get("/kuhusu")
def kuhusu():
    return {"jina": "Amina", "kozi": "Python", "wiki": 9}


# 3.2
@app.get("/hesabu/jumla/{a}/{b}")
def jumla(a: float, b: float):
    return {"a": a, "b": b, "jumla": a + b}
# /hesabu/jumla/2.5/4 -> {"a":2.5,"b":4.0,"jumla":6.5}


# 3.4 (imeongezwa 'panga')
@app.get("/bidhaa")
def orodha_bidhaa(
    aina: str | None = None,
    bei_max: int | None = None,
    limit: int = 10,
    panga: bool = False,
):
    matokeo = [
        {"id": bid, **b}
        for bid, b in BIDHAA.items()
        if (aina is None or b["aina"] == aina)
        and (bei_max is None or b["bei"] <= bei_max)
    ]
    if panga:
        matokeo.sort(key=lambda b: b["bei"])
    return matokeo[:limit]


# 3.3 — LAZIMA iwe juu ya /bidhaa/{bidhaa_id}
@app.get("/bidhaa/tafuta")
def tafuta_bidhaa(neno: str):
    neno = neno.lower()
    return [
        {"id": bid, **b}
        for bid, b in BIDHAA.items()
        if neno in b["jina"].lower()
    ]
# /bidhaa/tafuta?neno=SU -> [{"id":1,"jina":"Sukari","bei":3000,"aina":"chakula"}]


@app.get("/bidhaa/{bidhaa_id}")
def pata_bidhaa(bidhaa_id: int):
    if bidhaa_id not in BIDHAA:
        return {"kosa": "Bidhaa haipo"}
    return {"id": bidhaa_id, **BIDHAA[bidhaa_id]}

/bidhaa?panga=true&limit=3 → Kalamu (500), Daftari (1000), Sabuni (1500).


Homework

To submit: next session.

Task A — "Library API" (FastAPI). Create main.py with a list of at least 6 books (each one: id, kichwa (title), mwandishi (author), mwaka (year), lugha (language) — "sw" or "en"). Endpoints:

  1. GET / → a welcome message.
  2. GET /vitabu → query params: lugha: str | None = None, baada_ya: int | None = None (books with a year greater than this), limit: int = 10.
  3. GET /vitabu/{kitabu_id} → one book; if it doesn't exist return {"kosa": "Kitabu hakipo"}.
  4. GET /waandishi → a list of author names without repeats, sorted alphabetically.

Task B — Client (requests). Create mteja.py, which calls your API (with the server running) and prints: (1) the number of Swahili books, (2) book number 2, (3) the list of authors. Use timeout and raise_for_status().

Task C — Questions (write them in majibu.md):

  1. Explain in your own words the difference between a path parameter and a query parameter, with your own example.
  2. Name 3 status codes from the 4xx group and what they mean.
  3. Why does your GET /vitabu/99 return 200 even though the book doesn't exist, and which status would be appropriate?
Answer

Task A — main.py

python
from fastapi import FastAPI

app = FastAPI(title="API ya Maktaba")

VITABU = [
    {"id": 1, "kichwa": "Kusadikika", "mwandishi": "Shaaban Robert", "mwaka": 1951, "lugha": "sw"},
    {"id": 2, "kichwa": "Rosa Mistika", "mwandishi": "Euphrase Kezilahabi", "mwaka": 1971, "lugha": "sw"},
    {"id": 3, "kichwa": "Things Fall Apart", "mwandishi": "Chinua Achebe", "mwaka": 1958, "lugha": "en"},
    {"id": 4, "kichwa": "Kiu", "mwandishi": "Mohammed Suleiman Mohammed", "mwaka": 1972, "lugha": "sw"},
    {"id": 5, "kichwa": "Weep Not, Child", "mwandishi": "Ngugi wa Thiong'o", "mwaka": 1964, "lugha": "en"},
    {"id": 6, "kichwa": "Utubora Mkulima", "mwandishi": "Shaaban Robert", "mwaka": 1968, "lugha": "sw"},
]


@app.get("/")
def mwanzo():
    return {"ujumbe": "Karibu Maktaba API"}


@app.get("/vitabu")
def orodha_vitabu(lugha: str | None = None, baada_ya: int | None = None, limit: int = 10):
    matokeo = [
        k for k in VITABU
        if (lugha is None or k["lugha"] == lugha)
        and (baada_ya is None or k["mwaka"] > baada_ya)
    ]
    return matokeo[:limit]


@app.get("/vitabu/{kitabu_id}")
def pata_kitabu(kitabu_id: int):
    for k in VITABU:
        if k["id"] == kitabu_id:
            return k
    return {"kosa": "Kitabu hakipo"}


@app.get("/waandishi")
def waandishi():
    return sorted({k["mwandishi"] for k in VITABU})

Note: /waandishi doesn't clash with /vitabu/{kitabu_id} because the paths are different.

Task B — mteja.py

python
import requests

BASE = "http://127.0.0.1:8000"


def get(path: str, **params):
    r = requests.get(f"{BASE}{path}", params=params, timeout=5)
    r.raise_for_status()
    return r.json()


def main() -> None:
    try:
        vya_sw = get("/vitabu", lugha="sw")
        print("Vitabu vya Kiswahili:", len(vya_sw))
        print("Kitabu namba 2:", get("/vitabu/2")["kichwa"])
        print("Waandishi:", ", ".join(get("/waandishi")))
    except requests.ConnectionError:
        print("Server haiendeshi. Endesha: fastapi dev main.py")


if __name__ == "__main__":
    main()

Output:

text
Vitabu vya Kiswahili: 4
Kitabu namba 2: Rosa Mistika
Waandishi: Chinua Achebe, Euphrase Kezilahabi, Mohammed Suleiman Mohammed, Ngugi wa Thiong'o, Shaaban Robert

Task C

  1. A path param identifies one specific thing (/vitabu/2 = book number 2); a query param filters/adjusts the results (/vitabu?lugha=sw = Swahili books only).
  2. 400 Bad Request (a bad request), 401 Unauthorized (not logged in), 404 Not Found (doesn't exist), 422 (incorrect data).
  3. Because we just return a dict and FastAPI sets 200 by default. It should be 404 Not Found (we will use HTTPException in Week 10).

If you are asked a question you don't know

Say honestly: "Good question — I'm not sure, let's look it up together" then search in front of the student (that is a lesson too: knowing how to search). Search in English, for example "fastapi query parameter list", "requests post json headers", "http status 409 meaning". Official sources: FastAPI Tutorial https://fastapi.tiangolo.com/tutorial/ · requests https://requests.readthedocs.io/ · MDN HTTP https://developer.mozilla.org/en-US/docs/Web/HTTP · JSONPlaceholder guide https://jsonplaceholder.typicode.com/guide/


End-of-week checklist

  • Explains client/server and request/response with their own analogy
  • Breaks up a URL: scheme, host, port, path, query
  • Knows what GET, POST, PUT, PATCH, DELETE do
  • Knows 200, 201, 204, 400, 401, 403, 404, 422, 500
  • Uses DevTools → Network to see status and headers
  • Uses requests.get with params=, requests.post with json=
  • Always sets timeout= and uses raise_for_status() inside try/except
  • Installs "fastapi[standard]" inside the venv and runs fastapi dev main.py
  • Writes path parameters and query parameters with type hints (including optional | None = None)
  • Understands why a 422 happens and reads its detail
  • Knows the route order problem
  • Uses /docs → "Try it out"
  • Homework is on GitHub and .venv is not in the repo