REST API Design Checkpoint
25 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Fix the mistakes
The slips that cost marks in this module. Spot each one, then check the fix.
A good API is boring in the best way: the URLs are guessable, lists come in pages, errors look the same everywhere and old clients keep working. This checkpoint builds a small orders API with all four and then tests the module.
Review the routes first
Before coding, list the routes. Each should be a plural noun with the method as the verb. Look for verbs in paths, singular names and deep nesting.
routes = [ ("GET", "/v1/orders"), ("POST", "/v1/orders"), ("GET", "/v1/orders/{id}"), ("DELETE", "/v1/orders/{id}"),]print(len(routes))Four short routes cover the whole resource.
A list endpoint needs limits and filters
The list route takes skip, limit and a filter such as status. Bound the limit. Return the total so the client can show page numbers.
def page(rows, skip, limit, status=None): rows = [r for r in rows if status in (None, r["status"])] return {"total": len(rows), "items": rows[skip:skip + limit]}status in (None, r["status"]) keeps every row when no filter is given.
Errors and versions stay predictable
A missing order is 404 and a clash is 409, both in the shared error body. A rename of a field is a new version; a new optional field is not.
def needs_new_version(removed, retyped): return bool(removed or retyped)
print(needs_new_version(set(), set()))Example 1 · A small versioned orders API
from fastapi import APIRouter, FastAPI, Query, Responsefrom fastapi.responses import JSONResponsefrom fastapi.testclient import TestClientfrom pydantic import BaseModel
app = FastAPI()v1 = APIRouter(prefix="/v1")orders = [ {"id": 1, "status": "paid", "total": 40}, {"id": 2, "status": "open", "total": 15}, {"id": 3, "status": "paid", "total": 95},]
class NewOrder(BaseModel): total: int
def error(status, code, message): return JSONResponse(status_code=status, content={"error": {"code": code, "message": message}})
@v1.get("/orders")def list_orders(status: str | None = None, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100)): rows = [o for o in orders if status in (None, o["status"])] return {"total": len(rows), "items": rows[skip:skip + limit]}
@v1.post("/orders", status_code=201)def create(data: NewOrder, response: Response): order = {"id": len(orders) + 1, "status": "open", "total": data.total} orders.append(order) response.headers["Location"] = f"/v1/orders/{order['id']}" return order
@v1.get("/orders/{oid}")def read(oid: int): for o in orders: if o["id"] == oid: return o return error(404, "not_found", "order not found")
app.include_router(v1)client = TestClient(app)print(client.get("/v1/orders?status=paid&limit=1").json())r = client.post("/v1/orders", json={"total": 20})print(r.status_code, r.headers["location"])print(client.get("/v1/orders/9").json()){'total': 2, 'items': [{'id': 1, 'status': 'paid', 'total': 40}]}
201 /v1/orders/4
{'error': {'code': 'not_found', 'message': 'order not found'}}What this code does
- The list route filters first, then pages, then reports the total after filtering.
- Create answers 201 with a Location header; a missing order uses the shared error body.
Example 2 · A tiny route reviewer
def review(method, path): notes = [] parts = [p for p in path.split("/") if p and not p.startswith("{")] if any(p.lower().startswith(("get", "set", "create", "delete")) for p in parts): notes.append("verb in path") if any(p != p.lower() for p in parts): notes.append("use lower case") if method == "POST" and path.rstrip("/").split("/")[-1].startswith("{"): notes.append("POST on an item: use PUT or PATCH") return notes or ["ok"]
for method, path in [("GET", "/v1/orders"), ("POST", "/v1/createOrder"), ("POST", "/v1/Orders/{id}")]: print(method, path, "->", review(method, path))GET /v1/orders -> ['ok']
POST /v1/createOrder -> ['verb in path', 'use lower case']
POST /v1/Orders/{id} -> ['use lower case', 'POST on an item: use PUT or PATCH']What this code does
- A script like this can be part of a code review.
- The rules are the ones from this module: nouns, lower case and the right method.
Nothing removed or retyped, so the current version can stay.
Finish up
Mark complete & continue
Next: Calling APIs with httpx and requests