1 The Problem
We want a REST API: a web service that lets clients create, read, update, and delete records (a to-do list, say) using standard HTTP methods (GET, POST, PUT, DELETE) and JSON. It teaches the conventions of REST — how the entire web of apps and services talks to itself.
2 How to Think About It
Think about how REST maps actions to HTTP, before any code:
/todos/3. → 2. The HTTP method says what to do: GET reads, POST creates, PUT updates, DELETE removes. → 3. The body carries JSON data. → 4. The server routes each request to the right handler and returns JSON. This method-plus-URL convention is REST.
3 The Build — explained part by part
Here is a complete REST API using only Python’s built-in http.server — no framework needed. Each part is explained below.
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
todos = {} # id -> todo dict
next_id = 1
class Handler(BaseHTTPRequestHandler):
def _send(self, code, data):
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def do_GET(self):
# GET /todos -> list every todo.
self._send(200, list(todos.values()))
def do_POST(self):
# POST /todos -> create a todo from the JSON body.
global next_id
length = int(self.headers["Content-Length"])
body = json.loads(self.rfile.read(length))
todo = {"id": next_id, "task": body["task"], "done": False}
todos[next_id] = todo
next_id += 1
self._send(201, todo)
def do_DELETE(self):
# DELETE /todos/3 -> remove that todo.
todo_id = int(self.path.split("/")[-1])
if todo_id in todos:
del todos[todo_id]
self._send(200, {"deleted": todo_id})
else:
self._send(404, {"error": "not found"})
if __name__ == "__main__":
HTTPServer(("localhost", 8000), Handler).serve_forever()
http.server: name a method do_GET and it runs for every GET request. The HTTP method picks the handler automatically — that is REST’s method convention in action._send(code, data) — a helper that sends a status code (200 OK, 201 Created, 404 Not Found) and a JSON body. Reusing it keeps every response consistent.
do_POST — read the
Content-Length to know how many bytes the body is, parse the JSON, build a new todo with the next ID, and return it with 201 Created.self.path.split("/")[-1] — pull the ID out of a URL like
/todos/3 by taking the last path piece.status codes matter —
201 for created, 404 for not found. Clients rely on these codes to know what happened.
Content-Length — you read too few or too many bytes of the body.Content-Length bytes before parsing JSON.4 Test & Prove Each Part
We test the data operations — create and delete — directly, separate from the HTTP layer, so tests are fast and need no running server.
def create_todo(store, next_id, task):
todo = {"id": next_id, "task": task, "done": False}
store[next_id] = todo
return todo
def delete_todo(store, todo_id):
if todo_id in store:
del store[todo_id]
return True
return False
def test_create():
"""Creating stores a todo with its ID."""
store = {}
todo = create_todo(store, 1, "Buy milk")
assert todo["id"] == 1
assert store[1]["task"] == "Buy milk"
def test_delete():
"""Deleting removes the todo."""
store = {1: {"id": 1}}
assert delete_todo(store, 1) is True
assert 1 not in store
def test_delete_missing():
"""Deleting a missing todo returns False."""
assert delete_todo({}, 99) is False
Run with pytest -v. We test the data logic apart from the HTTP plumbing — a key technique. The handlers just connect HTTP to these tested operations.
5 API Documentation
The API’s endpoints, documented like any professional REST service.
Request body
{"task": "Buy milk"}Response (201)
{"id": 1, "task": "Buy milk", "done": false}6 Run It & Automate It
python3 api.pyThen in another terminal:
curl -X POST localhost:8000/todos -d '{"task":"Test"}' to create, and curl localhost:8000/todos to list.Jenkins runs the tests automatically — each line explained below.
# POST /todos {"task":"Buy milk"}
{"id": 1, "task": "Buy milk", "done": false}
# GET /todos
[{"id": 1, "task": "Buy milk", "done": false}]KeyError: 'Content-Length'..encode().// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Add PUT. Implement updating a todo’s done status. (Teaches: the update half of CRUD.)
- Persist to disk. Save todos to JSON so they survive restarts. (Teaches: combining with persistence.)
- Use a framework. Rebuild it with Flask or FastAPI. (Teaches: real-world tools.)