Skip to Content
Mélodium 0.10.3 is now available!
DocsExamples06. HTTP Server API

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 8080
curl 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