calc_service exposes POST /evaluate: send an expression, optionally with variable bindings, get back a result — or, for a malformed expression or a genuine contract violation like division by zero, a 400 response with an error body instead of a crash. The interpreter behind it is Chapter 4's expr.nex, embedded exactly as that chapter described: a library built for exactly this kind of reuse, with no idea it is being called over HTTP at all.
Problem
The interpreter's own contracts — require non_zero, require known — are stated in terms of Nex's control flow: a violation is a program failure, caught with rescue. HTTP has no such mechanism. A network client cannot catch a Nex precondition violation; it can only read a status code and a body. So this project's real design work is a translation layer: turning "the interpreter's contract was violated" into "the HTTP contract says this was the client's fault" — status 400, with an error message describing what actually went wrong, not a crashed connection or a generic 500.
The request and response shapes are settled first, the same discipline as every earlier chapter:
POST /evaluate
{"expr": "1 + x", "vars": {"x": 2}}
200 {"result": 3.0}
400 {"error": "Precondition violation: known"}
Design as Contract
Calc_Service's constructor registers exactly one route, and the handler is an ordinary method on the service itself:
class Calc_Service
create
make(port: Integer) do
this.server := create Http_Server.make(port)
this.json := create Json.make()
server.post("/evaluate", fn(req: Http_Request): Http_Server_Response do
result := this.handle_evaluate(req)
end)
end
That last line is doing more design work than it looks like — the route closure's entire body is a single call out to this.handle_evaluate(req), which means the actual request-handling logic lives as a normal, testable method on Calc_Service, not smeared across an anonymous callback. The closure's only job is to be the thing Http_Server's API wants; everything a reader would actually want to understand about how this service behaves is one method away, in ordinary object-oriented shape.
The translation from interpreter contract to HTTP status lives in one place, wrapping the interpreter call in exactly the boundary this project exists to build:
handle_evaluate(req: Http_Request): Http_Server_Response do
do
let body: Map[String, Any] := json.parse(req.body())
let expr_text: String := body.get("expr")
let vars: Map[String, Real] := parse_vars(body)
let value: Real := evaluate(expr_text, vars)
let payload: Map[String, Any] := {}
payload.set("result", value)
result := json_response(200, payload)
rescue
let payload: Map[String, Any] := {}
payload.set("error", "" + exception)
result := json_response(400, payload)
end
end
Every way evaluate can fail — a malformed expression the parser rejects, a division by zero, an undefined variable — is a Nex-level failure caught by the same rescue, and every one of them becomes the same shape of HTTP response: 400, with the failure's own message as the error text. The interpreter never had to change to support this; Chapter 4's contract violations already carried exactly the information this boundary needed, which is the payoff of having designed them as labeled failures in the first place rather than silent wrong answers.
Build
The complete, current code for this project is at examples/contracts_at_work/07_http_calc on GitHub.
One conversion detail is worth a look, because it is exactly the kind of small seam where two systems' type models disagree: JSON has one number type; Nex's parser distinguishes Integer from Real, and evaluate wants every variable as a Real regardless of how the client happened to write it in the request body:
-- JSON integers parse as Integer, JSON decimals as Real; evaluate wants
-- Real either way.
to_real_any(v: Any): Real do
if type_is("Integer", v) then
let n: Integer := v
result := n.to_real()
else
result := v
end
end
{"x": 2} and {"x": 2.0} both have to reach evaluate as the same kind of value, and this is the one place that reconciliation happens — a small, boring, necessary piece of code that exists entirely because two systems (JSON's number model, Nex's type system) meet at this boundary and do not agree on how many number types there are.
json_response closes the loop on the other side, the one place a status code and a payload become an actual HTTP response with the right content type:
json_response(status_code: Integer, payload: Map[String, Any]): Http_Server_Response do
let headers: Map[String, String] := {}
headers.set("Content-Type", "application/json")
result := create Http_Server_Response.make(status_code, json.stringify(payload), headers)
end
Test
The check suite starts a real Calc_Service on an ephemeral port and drives it with a real Http_Client — the same commitment to integration testing over mocking that Chapter 6's chat tests made, applied to HTTP instead of raw sockets:
let r2: Http_Response := client.post(base + "/evaluate",
"{\"expr\":\"x * x + y\",\"vars\":{\"x\":3,\"y\":1}}")
let body2: Map[String, Any] := json.parse(r2.body())
c.check("vars result", "10.0", "" + body2.get("result"))
let r3: Http_Response := client.post(base + "/evaluate", "{\"expr\":\"1 / 0\"}")
c.check("division by zero status", "400", "" + r3.status())
let body3: Map[String, Any] := json.parse(r3.body())
c.check("division by zero has error", "true", "" + body3.contains_key("error"))
Notice what that last pair of checks actually proves: not just that a bad expression fails somehow, but that it fails with the specific, designed HTTP contract this project promises — status 400, an error key present in the body. A test that only checked "the request didn't crash the server" would miss the entire point of building the translation layer in the first place.
Takeaways
A library designed to fail with a labeled, specific reason — not a generic exception — pays for that design the moment it gets reused somewhere its author never anticipated. Embedding Chapter 4's interpreter behind an HTTP endpoint needed no changes to the interpreter itself, because every failure it already produced was exactly the information this new boundary needed to translate a contract violation into an HTTP status code. Design a failure to name what broke, and that design decision keeps paying out in contexts you can't yet foresee.
One process again in Chapter 8, but a boundary this book has not crossed yet: not the network this time, but the host platform's own GUI toolkit.