HTTP Server API
Source: tutorial/06_http_server_api See in Playground
A small HTTP server with three routes: a fixed status endpoint, one that reads request metadata from the @HttpRequest context, and one that parses a JSON body and replies with a JSON object built from it.
Running
cd tutorial/06_http_server_api
melodium run Compo.toml --port 8080curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/whoami
curl -X POST http://127.0.0.1:8080/greet -d '{"name":"Sláine"}'{"status":"ok"}
{"path":"/whoami","route":"/whoami"}
{"message":"thanks for the greeting!","received":"{\"name\":\"Sláine\"}"}Optional: add --api-report and an API token (MELODIUM_API_TOKEN) to see this run’s full trace on Cadence.CI .
How it works
A single HttpServer model is shared by all three routes. start binds the socket once at startup, and connection is instantiated once per route, creating a new track, with the @HttpRequest context available, for every matching incoming request:
treatment main(const port: u16 = 8080)
model server: HttpServer(host=|from_ipv4(|localhost_ipv4()), port=port)
{
startup()
start[http_server=server]()
logReady: logInfoMessage(label="server", message="HTTP server ready")
startup.trigger -> start.trigger
startup.trigger -> logReady.trigger
health[http_server=server]()
whoami[http_server=server]()
greet[http_server=server]()
}health, whoami, and greet are independent treatments, each owning its own connection instance; main just instantiates all three against the same server model. The data flow is the same shape for every route:
Driving a response from connection.started
GET /health returns a fixed JSON status:
treatment health[http_server: HttpServer]() {
connection[http_server=http_server](method=|get(), route="/health")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|map([]))
connection.started -> status.trigger,emit -> connection.status
connection.started -> headers.trigger,emit -> connection.headers
reply: emit<string>(value="{\"status\":\"ok\"}")
asBytes: stream<string>()
encoded: encode()
connection.started -> reply.trigger,emit -> asBytes.block,stream -> encoded.text,data -> connection.data
}The response is driven from connection.started, a Block<void> that fires as soon as the connection is accepted, rather than from a trigger derived from connection.data (the incoming body). A GET request has no body, so a stream never starts on connection.data, and anything gated on “first byte of the body” would simply never fire, leaving the route hanging. This is the one thing to get right in every route in this codebase: /greet (a POST with an actual body) would work either way, which is exactly the trap, since a body-derived trigger looks correct until tested against a route with no body.
Reading request metadata with @HttpRequest
GET /whoami reads @HttpRequest[route] and @HttpRequest[path] directly, without touching the request body at all:
treatment whoami[http_server: HttpServer]() {
connection[http_server=http_server](method=|get(), route="/whoami")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|map([]))
connection.started -> status.trigger,emit -> connection.status
connection.started -> headers.trigger,emit -> connection.headers
describe()
connection.started -> describe.trigger,body -> connection.data
}
treatment describe()
require @HttpRequest
input trigger: Block<void>
output body: Stream<byte>
{
info: emit<StringMap>(value=|insert(|entry("route", @HttpRequest[route]), "path", @HttpRequest[path]))
asJson: fromStringMap()
asText: toString<Json>()
asStream: stream<StringMap>()
encoded: encode()
Self.trigger -> info.trigger,emit -> asStream.block,stream -> asJson.value,json -> asText.value,into -> encoded.text,data -> Self.body
}describe declares require @HttpRequest, so Mélodium only allows it to be used inside a track that actually provides that context, which connection guarantees.
Echoing a parsed JSON body
POST /greet parses the JSON body and rebuilds a response with entry / insert and fromStringMap:
treatment greet[http_server: HttpServer]() {
connection[http_server=http_server](method=|post(), route="/greet")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|map([]))
connection.started -> status.trigger,emit -> connection.status
connection.started -> headers.trigger,emit -> connection.headers
respondGreeting()
connection.data -> respondGreeting.data,data -> connection.data
}There is no field-by-field access into a parsed Json value in the json package itself: reaching into {"name": "Sláine"} to pull out "Sláine" needs the JavaScript engine, covered in the next tutorial step. Here the whole body is echoed back as one JSON string value instead of reaching into it.
Dependencies
[dependencies]
std = "0.10.3" # core flows, logging, data structures
http = "0.10.3" # HTTP client and server
net = "0.10.3" # IP address helpers
json = "0.10.3" # JSON parsing and serialisation
encoding = "0.10.3" # UTF-8 encode / decode