← Teacher guide

An API for the club

Students turn the club's data into an API, with routes that return dictionaries sent as JSON, then add a stats route, a search with a query string and an API of their own for the results.

Web apps with Flask · APIs · about 45 minutes · Open the student lesson

Classroom resources

Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.

Students will be able to

Timing

Predict and Run 10 min · Investigate 10 min · Modify 15 min · Make 10 min

Before class

Read the unit primer first. A Flask route that returns a dictionary sends it as JSON (Flask sorts the keys); the App tab shows it neatly. An API is a set of addresses that send data for programs, not pages for people. The lesson reads members.csv from Lesson 4 (the Files tab can put the original back). The bug task returns the function load_members instead of calling it, giving 500 Internal Server Error (the console shows the error: a function cannot be sent as JSON). The query string (?name=ad) is read with request.args.get("name", ""), where the second value is used when it is missing. The info question raises privacy: an API shares data with anyone who asks.

Running the lesson

  1. Predict: compare the dictionary in the code with the JSON on screen.
  2. Investigate: ask which is easier for a program: the home page HTML or the JSON?
  3. Modify: the stats route is Data detective's counting dictionary again; point that out to students who did it.
  4. Info: discuss what in members.csv should not be public (if it had emails or year levels).

The hard part (on purpose)

Seeing JSON as data for programs, not a page. Ask how another program would use the reply, before adding routes.

When students get stuck

What you will seeA question to askThe fix
500 Internal Server Error and "not JSON serializable" in the console.Is something in the dictionary a function, or not a number, text, list or dictionary?Call the function: load_members().
The search finds nobody.Are both sides lower case?name in m["name"].lower(), with name lowered too.
Wins come out as "5" in quotes.Is wins text or a number in the dictionary?int(r["wins"]).

Answers to the checks

  1. Predict: Someone visits /api/club. What do they get?
    The dictionary as JSON. Flask turns a returned dictionary into JSON text, which looks almost exactly like the Python dictionary.
  2. Data for programs: A weather app shows the club's members. Why would it use /api/members, not the home page?
    JSON is data it can use straight away; a page is HTML made for people, which a program would have to pick apart
    A program wants the data itself. JSON gives it names and values; HTML mixes them up with headings, links and layout.
  3. An empty answer: What does /api/members/Nobody send?
    {"count": 0, "group": "Nobody", "members": []}
    The route still runs; the loop finds nobody, so count is 0 and members is an empty list. An empty answer is still a good answer for a program.
  4. Spot the bug: Visiting /api/members gives 500 Internal Server Error. Click the line that is wrong.
    line 14
    load_members without brackets is the function itself, which cannot be sent as JSON. load_members() runs it and gives the list.
  5. Where this leads: Anyone who knows the address can read /api/members. What should a real club API think about?
    Which data is safe to share, and whether some routes need a login
    An API shares data with any program that asks. Real APIs share only what they should, and protect the rest with logins and keys.

Solutions

One possible solution for each task. Other correct programs are fine.

Modify › Stats

import csv
from flask import Flask, request

app = Flask(__name__)


def load_members():
    with open("members.csv") as f:
        return list(csv.DictReader(f))


@app.route("/")
def home():
    page = "<h1>Chess club API</h1>"
    page = page + "<p><a href='/api/members'>/api/members</a></p>"
    page = page + "<p><a href='/api/members/Players'>/api/members/Players</a></p>"
    return page


@app.route("/api/members")
def api_members():
    return {"members": load_members()}


@app.route("/api/members/<group>")
def api_group(group):
    found = []
    for m in load_members():
        if m["group"] == group:
            found.append(m)
    return {"group": group, "count": len(found), "members": found}



@app.route("/api/stats")
def api_stats():
    counts = {}
    for m in load_members():
        if m["group"] in counts:
            counts[m["group"]] = counts[m["group"]] + 1
        else:
            counts[m["group"]] = 1
    return counts


app.run()

Modify › Search

import csv
from flask import Flask, request

app = Flask(__name__)


def load_members():
    with open("members.csv") as f:
        return list(csv.DictReader(f))


@app.route("/")
def home():
    page = "<h1>Chess club API</h1>"
    page = page + "<p><a href='/api/members'>/api/members</a></p>"
    page = page + "<p><a href='/api/members/Players'>/api/members/Players</a></p>"
    return page


@app.route("/api/members")
def api_members():
    name = request.args.get("name", "").lower()
    found = []
    for m in load_members():
        if name in m["name"].lower():
            found.append(m)
    return {"members": found}


@app.route("/api/members/<group>")
def api_group(group):
    found = []
    for m in load_members():
        if m["group"] == group:
            found.append(m)
    return {"group": group, "count": len(found), "members": found}


app.run()

a results API

import csv
from flask import Flask

app = Flask(__name__)


def load_results():
    with open("results.csv") as f:
        return list(csv.DictReader(f))


@app.route("/api/results")
def api_results():
    rows = []
    for r in load_results():
        rows.append({"player": r["player"], "wins": int(r["wins"])})
    return {"results": rows}


@app.route("/api/leader")
def api_leader():
    rows = load_results()
    leader = rows[0]
    for r in rows:
        if int(r["wins"]) > int(leader["wins"]):
            leader = r
    return {"player": leader["player"], "wins": int(leader["wins"])}


app.run()

Stretch and support

Stretch: Add /api/members/<group>/count that sends only a number, and explain why a program might want that.

Support: Give students the stats loop, and ask them only to write the route and the return.

Exit ticket

How is an API route different from a page route?

It sends data (JSON) for a program to use, instead of an HTML page for a person to read.

What each step is for

Where it leads

The optional FastAPI lesson builds the same API with type checks done for you. Data detective's optional API lesson calls real APIs such as Wikidata.