A small API
Build a small HTTP service with clear boundaries, tests, logs and a repeatable runtime.
THE IDEA
A handler is a small, honest function
An API endpoint is a function that receives a request and returns a response. Keep it to four jobs in this order: check the input, call the domain logic, store or fetch, report the outcome. The domain logic (like subtotal) stays a plain function from the previous lesson; the handler is the adapter around it.
def get_order(request):
# request = {"method": "GET", "params": {"id": "1"}, "json": None}
...
return 200, {"id": 1, "total": 370} # (status, body)Who is at fault decides the status
A good response says what happened. Follow a request through the stages below. The earliest stage that can reject it does, and each rejection has its own status. Try all nine scenarios.
POST /orders body: {"items":[{"qty":2,"price":125},{"qty":3,"price":40}]}
201 {"id":2,"total":370}The status says who is at fault: 4xx means the caller can change something and retry, 5xx means the service failed. The crash scenario shows why the safety net must not copy the exception text into the body.
- Malformed: the body is not JSON, or an id is not a number. The caller must send something different (400).
- Invalid: well-formed JSON with unacceptable values (422 in this design).
- Missing: everything is fine but nothing is there (404).
- Our fault: something unexpected broke (500).
Predict first. An unexpected exception happens while handling a request. What should the response be?
Validation traps
In Python True is an instance of int, so isinstance(True, int) is True. A validator that only checks isinstance(qty, int) will accept true from JSON as a quantity of 1. Decide your rules explicitly, test them with bad values, and make sure nothing is stored before validation succeeds.
Chapter context
Engineering makes a program understandable when it fails or changes. Separate pure calculations from external I/O so behavior can be checked cheaply and errors can be connected to specific inputs.
TRY IT YOURSELF
Exercise: three handlers and an error wrapper
- A hidden router parses the URL and the JSON body, then calls your function with a **request dict** like
{"method": "POST", "params": {"id": "2"}, "json": {...}}.request["json"]isNonewhen the body was missing or was not valid JSON. Your handler returns(status, body_dict). get_order: id not a whole number is **400**, unknown id is **404**, found is **200** with the order.create_order: no JSON is **400**.itemsmust be a non-empty list whose entries have a whole-numberqtyof at least 1 and a whole-numberpriceof at least 0 (a string or a boolean is not a number here), otherwise **422** with{"errors": {"items": "..."}}and nothing stored. A valid order is stored with a new id and returns **201** with{"id": ..., "total": ...}.with_errors(handler)returns a wrapped handler: normal results pass through, any unexpected exception becomes **500**{"error": "internal error"}and its message must not appear in the body.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Should an unexpected server exception be reported as a successful response?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
For money examples, use integer minor units rather than binary floating point. The subtotal is sum(quantity × unit_price), and the domain must specify rounding and tax rules before those operations are implemented.
Worked example
Two items cost 125 minor units each and three cost 40 each. The subtotal is 2×125 + 3×40 = 370. A test can exercise this calculation without starting an HTTP server. Request parsing and response formatting need separate checks.
Reference code
The following example uses the Python standard library and does not call a remote service.
def subtotal(items):
return sum(quantity * price for quantity, price in items)
assert subtotal([(2, 125), (3, 40)]) == 370
assert subtotal([]) == 0
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
Test coverage counts executed lines, not verified requirements. A container does not turn an unsafe operation into a safe one. JSON validity alone does not establish application validity.