Say What You Mean
A REST API talks through two vocabularies. The method says what the client wants: GET read, POST create, PUT replace, PATCH change a part, DELETE remove. The status code says how it went: 2xx success, 4xx the client's mistake, 5xx the server's.
A detail worth knowing: PUT is idempotent — sending it twice ends the same as once. POST is not: two POSTs create two resources.
The Right Method
The handler class routes by verb for you: the server calls do_GET for a GET, do_POST for a POST, do_DELETE for a DELETE — one method per verb. Creation answers 201 Created and points at the new resource with a Location header; deletion answers 204 No Content — success with nothing to say.
GET, POST and DELETE on one resource
Three verbs, three answers: the list comes back with 200, the creation with 201 and a Location, the deletion with 204 and an empty body.
Output: complete Python program
import threading
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class NotesHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"notes: shopping, ideas\n")
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
self.rfile.read(length) # the new note's JSON
self.send_response(201) # created
self.send_header("Location", "/notes/3")
self.end_headers()
self.wfile.write(b"created\n")
def do_DELETE(self):
self.send_response(204) # success, nothing to say
self.end_headers()
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), NotesHandler)
base = f"http://127.0.0.1:{server.server_address[1]}"
threading.Thread(target=server.serve_forever, daemon=True).start()
with urllib.request.urlopen(base + "/notes") as resp:
print("GET /notes ->", resp.status, resp.read().decode().strip())
req = urllib.request.Request(base + "/notes", data=b'{"text": "call Ben"}',
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
print("POST /notes ->", resp.status, "Location:", resp.headers["Location"])
req = urllib.request.Request(base + "/notes/3", method="DELETE")
with urllib.request.urlopen(req) as resp:
print("DELETE /notes/3 ->", resp.status, "(no content)")
server.shutdown()
The Right Status Code
Errors deserve precision too: a missing resource is 404 Not Found (with a JSON error body so machines can read it), and a wrong verb on a read-only route is 405 Method Not Allowed — here answered by a tiny do_DELETE. (Leave that method out entirely and http.server answers 501 Unsupported method on its own — writing the 405 yourself is the more precise REST answer.)
On the client side, urllib raises HTTPError for every 4xx and 5xx answer instead of returning it quietly — the error object still carries the status code and the body.
200, 404 and 405
The existing note returns 200 with data, the missing one 404 with a JSON error, and the DELETE is refused with 405. The client catches HTTPError and reads the error bodies from it.
Output: complete Python program
import json
import threading
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
notes = {"1": "shopping list"}
class NoteHandler(BaseHTTPRequestHandler):
def do_GET(self):
note_id = self.path.rsplit("/", 1)[-1]
text = notes.get(note_id)
if text is None:
self.reply(404, {"error": "note not found"})
else:
self.reply(200, {"id": note_id, "text": text})
def do_DELETE(self):
# This API only reads notes - refuse every other verb honestly.
self.reply(405, {"error": "method not allowed"})
def reply(self, status, data):
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write((json.dumps(data) + "\n").encode())
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), NoteHandler)
base = f"http://127.0.0.1:{server.server_address[1]}"
threading.Thread(target=server.serve_forever, daemon=True).start()
def show(method, path):
req = urllib.request.Request(base + path, method=method)
try:
with urllib.request.urlopen(req) as resp:
status, body = resp.status, resp.read().decode()
except urllib.error.HTTPError as err: # raised for every 4xx and 5xx
status, body = err.code, err.read().decode()
print(f"{method:<6} {path} -> {status} {body.strip()}")
show("GET", "/notes/1")
show("GET", "/notes/99")
show("DELETE", "/notes/1")
server.shutdown()