anarchie serve
Serve the openEHR REST API over HTTP. The server binds to localhost and is built
on tiny_http - a plain blocking HTTP server, with no async runtime.
Usage
anarchie serve [--addr host:port]
| Option | Default | Description |
|---|---|---|
--addr <addr> |
127.0.0.1:8080 |
Address to bind, as host:port. |
$ anarchie serve --addr 127.0.0.1:8137
anarchie REST API listening on http://127.0.0.1:8137 (Ctrl-C to stop)
Endpoints
| Method | Path | What it does |
|---|---|---|
POST |
/v1/ehr |
Create an EHR. 201 with a Location header. |
GET |
/v1/ehr/{id} |
Fetch an EHR. |
POST |
/v1/ehr/{id}/composition |
Commit a Composition. 201 with ETag (the version_uid) and Location. |
GET |
/v1/ehr/{id}/composition/{uid} |
Fetch a Composition - a head object id or a full obj::sys::N version uid. |
PUT |
/v1/ehr/{id}/composition/{uid} |
Commit a new version. Requires If-Match; 412 on a stale precondition. |
GET/POST |
/v1/query/aql |
Run an ad-hoc AQL query (q as a query parameter, or in the JSON body). |
GET |
/v1/query/{name}[/{version}] |
Run a stored query. |
GET |
/v1/definition/template/adl1.4[/{id}] |
List templates, or fetch one. |
Validation failures return 422 with the same structured report you get from
anarchie validate.
A read model that is always current
The query endpoints refresh the index incrementally before running -
only EHRs whose git HEAD moved are re-indexed. A Composition committed over
REST is therefore immediately queryable, with no separate anarchie index step.
(The CLI query commands do not do this; they read the index as it stands.)
Example session
Start the server, then in another shell:
# Create an EHR (note the 201 and the Location header)
$ curl -s -i -X POST http://127.0.0.1:8137/v1/ehr
HTTP/1.1 201 Created
Server: tiny-http (Rust)
Content-Type: application/json
Location: /v1/ehr/6aeeffb0-0f9e-41be-be78-0e3d0d8028c1
{
"_type": "EHR",
"ehr_id": { "_type": "HIER_OBJECT_ID", "value": "6aeeffb0-0f9e-41be-be78-0e3d0d8028c1" },
"system_id": { "_type": "HIER_OBJECT_ID", "value": "demo.local" },
"time_created": { "value": "2026-06-22T12:07:25Z" }
}
# Commit a Composition into it (201, with the version_uid in the ETag)
$ EHR=6aeeffb0-0f9e-41be-be78-0e3d0d8028c1
$ curl -s -i -X POST "http://127.0.0.1:8137/v1/ehr/$EHR/composition" \
-H "Content-Type: application/json" \
--data-binary @blood-pressure-composition.json
HTTP/1.1 201 Created
Server: tiny-http (Rust)
Content-Type: application/json
ETag: "1e412356-46a6-4e94-9960-7eff11eb96f8::demo.local::1"
Location: /v1/ehr/6aeeffb0-0f9e-41be-be78-0e3d0d8028c1/composition/1e412356-46a6-4e94-9960-7eff11eb96f8::demo.local::1
# Query it straight away - the index refreshed itself first
$ curl -s -X POST http://127.0.0.1:8137/v1/query/aql \
-H "Content-Type: application/json" \
--data '{"q":"SELECT COUNT(*) FROM COMPOSITION c"}'
{
"q": "SELECT COUNT(*) FROM COMPOSITION c",
"columns": [
{
"name": "#count"
}
],
"rows": [
[
1
]
]
}
A Composition that breaches its template is rejected with 422 and the report:
$ curl -s -i -X POST "http://127.0.0.1:8137/v1/ehr/$EHR/composition" \
-H "Content-Type: application/json" --data-binary @bad.json
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"message": "Composition failed validation",
"validation": {
"valid": false,
"violations": [
{
"severity": "error",
"rm_path": "/content[openEHR-EHR-OBSERVATION.blood_pressure.v2]/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude",
"constraint": "C_DV_QUANTITY",
"message": "magnitude 5000 outside permitted range for units \"mm[Hg]\""
}
]
}
}
A PUT whose If-Match does not match the current head is rejected with 412,
so concurrent edits cannot silently clobber one another:
$ curl -s -i -X PUT "http://127.0.0.1:8137/v1/ehr/$EHR/composition/1e412356-46a6-4e94-9960-7eff11eb96f8" \
-H "Content-Type: application/json" \
-H 'If-Match: "1e412356-46a6-4e94-9960-7eff11eb96f8::demo.local::99"' \
--data-binary @blood-pressure-composition.json
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
{
"message": "If-Match 1e412356-46a6-4e94-9960-7eff11eb96f8::demo.local::99 does not match current version Some(\"1e412356-46a6-4e94-9960-7eff11eb96f8::demo.local::1\")"
}