Tiny REST API Demo in Python

2020-08-17

Introduction

The other day, someone asked me about the difference between “an API and a regular webpage.” After understanding more about the context of their question, I tried to come up with a decent explanation about the differences between a server sending HTML pages and one handling REST API requests. As a thousand words leave not the same deep impression as does a single deed, I thought I’d put together a tiny demonstration that serves HTML to a web browser and JSON over a REST endpoint. My demo uses the Flask microframework to handle the details; you can find a copy here, though we’ll go through some of it below.

How smol?

Before we get into the specifics, here’s how tiny the demo is: the whole thing, including a flake.nix file to set the stage, is under 115 lines.

~/github/tiny_api_demo ∃ tokei
===============================================================================
 Language            Files        Lines         Code     Comments       Blanks
===============================================================================
 CSS                     1           10           10            0            0
 HTML                    3           41           41            0            0
 Markdown                1           10            0            6            4
 Nix                     1           25           23            0            2
 Python                  2           54           40            2           12
===============================================================================
 Total                   8          140          114            8           18
===============================================================================

This whole thing cuts so many corners it might as well be a sphere. It’s certainly not a production-ready example by any stretch of the imagination; it’s only enough for demonstration purposes.

Preliminaries

First, we need some data. Pretend we’re creating the School Reporter 5000™, wherein we have student information that consists of a collection of (timestamp, person, activity) tuples. In data.py, we generate some fake data. Our application code will interface with this data via NAMES and SELECT; pretend this stands in for a proper database.

from datetime import datetime, timedelta
from itertools import accumulate
from operator import add
import random

random.seed(1729)

NAMES = ["Alice", "Bob", "Carol", "Dave"]
_ACTIVITIES = ["Reading", "Writing", "Arithmetic"]
_DATA = [
    {
        "timestamp": ts.time().isoformat(timespec="minutes"),
        "person": random.choice(NAMES),
        "activity": random.choice(_ACTIVITIES),
    }
    for ts in accumulate(
        (timedelta(minutes=random.randrange(17)) for _ in range(42)),
        func=add,
        initial=datetime.today().replace(hour=9),
    )
]
SELECT = {name: [x for x in _DATA if x["person"] == name] for name in NAMES}

App

The School Reporter 5000™ has three routes, all described in app.py. We serve HTML for the homepage at /index.html and individual person pages at /person/<name> for each of the names in NAMES. We also serve JSON in response to GET requests via the /api/<name> route. Thanks to Flask, we even have rudimentary error handling.

from flask import Flask, jsonify, render_template
from data import NAMES, SELECT

app = Flask(__name__)

@app.route("/")
def home():
    return render_template("index.html", names=NAMES)

@app.route("/person/<string:name>")
def person(name):
    try:
        return render_template("person.html", name=name, data=SELECT[name], error=False)
    except KeyError:
        return render_template("person.html", name=name, error=True), 400

@app.route("/api/<string:name>")
def api(name):
    try:
        return jsonify(SELECT[name])
    except KeyError:
        return {"error": f"Unknown person {name}"}, 400

With the server running, we can visit the home page:

Homepage

Or report pages for any specific student:

Alice's page

However, we can also get JSON data for individual students by pinging the REST endpoint; using, e.g. HTTPie to query the endpoint via http --body :5000/api/Dave, we get

Dave's JSON

Everything Else

To round out the example, we also have some minimal CSS and two other HTML templates. Here’s the flake.nix to specify the W O R L D:

{
  description = "Tiny API Demo";

  inputs = {
    flake-utils.url = "github:numtide/flake-utils";
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
  };

  outputs = {
    self,
    flake-utils,
    nixpkgs,
  }:
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = import nixpkgs {inherit system;};
      py = pkgs.python310.withPackages (p: [p.flask]);
    in {
      packages.default = pkgs.writeShellApplication {
        name = "tiny_api_demo";
        text = ''
          FLASK_APP=app.py ${py}/bin/python -m flask run
        '';
      };
    });
}

From the base directory, one can run nix run to set up the environment and kick off the server.