Skip to content

Iron Log MD: the wire

This page shows what Iron Log MD sends and receives over HTTP. The generated Axum server speaks JSON:API 1.1: every request and response body is application/vnd.api+json. The wire contract is the full specification, the server transports guide explains how the routes are generated, and Upgrading lists what changed for existing clients.

To run the server yourself:

Terminal window
cd examples/iron-log-md
PORT=39101 cargo run # without PORT it serves on 3001

Every pair below was captured from a real run against a scratch copy of data/, with curl -sg -i. The requests ran in the order shown, so each one sees the writes before it. Response bodies are formatted with jq. Of the response headers, only the status line, Content-Type, Location, Allow and Vary are shown.

The four resource types are exercises, tags, workouts and workout-sets. Iron Log MD configures no pagination, so its lists are not paged; they sort and include. Its id strategy is IdStrategy::Provided: the client supplies every id. The generated server is the same as Iron Log’s on SQLite; only the store underneath differs, so the documents on the two pages have the same shape.

Contents

The client sends the id. The tags relationship links the seeded strength tag, and the response is 201 with a Location header. The record is written to data/vault/workouts/w-2026-06-03.md, with the tag as a [[strength]] wikilink.

Request
POST /api/workouts HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
"data": {
"type": "workouts",
"id": "w-2026-06-03",
"attributes": {
"name": "Squat volume",
"date": "2026-06-03",
"duration_minutes": 50,
"created_at": "2026-06-03T07:15:00Z"
},
"relationships": {
"tags": {
"data": [
{
"type": "tags",
"id": "strength"
}
]
}
}
}
}
Response
HTTP/1.1 201 Created
content-type: application/vnd.api+json
vary: Accept
location: /api/workouts/w-2026-06-03
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "/api/workouts/w-2026-06-03"
},
"data": {
"type": "workouts",
"id": "w-2026-06-03",
"attributes": {
"name": "Squat volume",
"date": "2026-06-03",
"duration_minutes": 50,
"notes": null,
"created_at": "2026-06-03T07:15:00Z"
},
"relationships": {
"tags": {
"links": {
"self": "/api/workouts/w-2026-06-03/relationships/tags",
"related": "/api/workouts/w-2026-06-03/tags"
},
"data": [
{
"type": "tags",
"id": "strength"
}
]
}
},
"links": {
"self": "/api/workouts/w-2026-06-03"
}
}
}

A workout set has two to-one relationships, workout and exercise. Both targets are checked to exist before the write.

Request
POST /api/workout-sets HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
"data": {
"type": "workout-sets",
"id": "w-2026-06-03-squat-1",
"attributes": {
"set_number": 1,
"weight_grams": 100000,
"reps": 5
},
"relationships": {
"workout": {
"data": {
"type": "workouts",
"id": "w-2026-06-03"
}
},
"exercise": {
"data": {
"type": "exercises",
"id": "back-squat"
}
}
}
}
}
Response
HTTP/1.1 201 Created
content-type: application/vnd.api+json
vary: Accept
location: /api/workout-sets/w-2026-06-03-squat-1
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1"
},
"data": {
"type": "workout-sets",
"id": "w-2026-06-03-squat-1",
"attributes": {
"set_number": 1,
"weight_grams": 100000,
"reps": 5,
"rpe": null,
"notes": null
},
"relationships": {
"workout": {
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/workout",
"related": "/api/workout-sets/w-2026-06-03-squat-1/workout"
},
"data": {
"type": "workouts",
"id": "w-2026-06-03"
}
},
"exercise": {
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/exercise",
"related": "/api/workout-sets/w-2026-06-03-squat-1/exercise"
},
"data": {
"type": "exercises",
"id": "back-squat"
}
}
},
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1"
}
}
}

A second set, w-2026-06-03-squat-2, was created the same way; that pair is omitted.

sort=-date sorts descending. The list is not paginated, so the document has no meta and no pagination links, only self.

Request
GET /api/workouts?sort=-date HTTP/1.1
Accept: application/vnd.api+json
Response
HTTP/1.1 200 OK
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "/api/workouts?sort=-date"
},
"data": [
{
"type": "workouts",
"id": "w-2026-06-03",
"attributes": {
"name": "Squat volume",
"date": "2026-06-03",
"duration_minutes": 50,
"notes": null,
"created_at": "2026-06-03T07:15:00Z"
},
"relationships": {
"tags": {
"links": {
"self": "/api/workouts/w-2026-06-03/relationships/tags",
"related": "/api/workouts/w-2026-06-03/tags"
},
"data": [
{
"type": "tags",
"id": "strength"
}
]
}
},
"links": {
"self": "/api/workouts/w-2026-06-03"
}
},
{
"type": "workouts",
"id": "w-2026-06-01",
"attributes": {
"name": "Leg day",
"date": "2026-06-01",
"duration_minutes": 45,
"notes": null,
"created_at": "2026-06-01T07:30:00Z"
},
"relationships": {
"tags": {
"links": {
"self": "/api/workouts/w-2026-06-01/relationships/tags",
"related": "/api/workouts/w-2026-06-01/tags"
},
"data": [
{
"type": "tags",
"id": "strength"
}
]
}
},
"links": {
"self": "/api/workouts/w-2026-06-01"
}
}
]
}

include=workout,exercise adds both to-one targets to included.

Request
GET /api/workout-sets/w-2026-06-03-squat-1?include=workout,exercise HTTP/1.1
Accept: application/vnd.api+json
Response
HTTP/1.1 200 OK
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1?include=workout,exercise"
},
"data": {
"type": "workout-sets",
"id": "w-2026-06-03-squat-1",
"attributes": {
"set_number": 1,
"weight_grams": 100000,
"reps": 5,
"rpe": null,
"notes": null
},
"relationships": {
"workout": {
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/workout",
"related": "/api/workout-sets/w-2026-06-03-squat-1/workout"
},
"data": {
"type": "workouts",
"id": "w-2026-06-03"
}
},
"exercise": {
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/exercise",
"related": "/api/workout-sets/w-2026-06-03-squat-1/exercise"
},
"data": {
"type": "exercises",
"id": "back-squat"
}
}
},
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1"
}
},
"included": [
{
"type": "workouts",
"id": "w-2026-06-03",
"attributes": {
"name": "Squat volume",
"date": "2026-06-03",
"duration_minutes": 50,
"notes": null,
"created_at": "2026-06-03T07:15:00Z"
},
"relationships": {
"tags": {
"links": {
"self": "/api/workouts/w-2026-06-03/relationships/tags",
"related": "/api/workouts/w-2026-06-03/tags"
},
"data": [
{
"type": "tags",
"id": "strength"
}
]
}
},
"links": {
"self": "/api/workouts/w-2026-06-03"
}
},
{
"type": "exercises",
"id": "back-squat",
"attributes": {
"name": "Back squat",
"muscle_group": "legs",
"equipment": "barbell",
"notes": null
},
"links": {
"self": "/api/exercises/back-squat"
}
}
]
}

A PATCH sets only the members it names; rpe goes from null to 8.

Request
PATCH /api/workout-sets/w-2026-06-03-squat-1 HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
"data": {
"type": "workout-sets",
"id": "w-2026-06-03-squat-1",
"attributes": {
"reps": 6,
"rpe": 8
}
}
}
Response
HTTP/1.1 200 OK
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1"
},
"data": {
"type": "workout-sets",
"id": "w-2026-06-03-squat-1",
"attributes": {
"set_number": 1,
"weight_grams": 100000,
"reps": 6,
"rpe": 8,
"notes": null
},
"relationships": {
"workout": {
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/workout",
"related": "/api/workout-sets/w-2026-06-03-squat-1/workout"
},
"data": {
"type": "workouts",
"id": "w-2026-06-03"
}
},
"exercise": {
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/exercise",
"related": "/api/workout-sets/w-2026-06-03-squat-1/exercise"
},
"data": {
"type": "exercises",
"id": "back-squat"
}
}
},
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1"
}
}
}

The related endpoint for a to-one relationship answers with the target resource itself.

Request
GET /api/workout-sets/w-2026-06-03-squat-1/exercise HTTP/1.1
Accept: application/vnd.api+json
Response
HTTP/1.1 200 OK
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "/api/workout-sets/w-2026-06-03-squat-1/exercise"
},
"data": {
"type": "exercises",
"id": "back-squat",
"attributes": {
"name": "Back squat",
"muscle_group": "legs",
"equipment": "barbell",
"notes": null
},
"links": {
"self": "/api/exercises/back-squat"
}
}
}

DELETE on a to-many relationship endpoint removes the identifiers it lists and answers 204. The seeded workout’s file loses its [[strength]] wikilink.

Request
DELETE /api/workouts/w-2026-06-01/relationships/tags HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
"data": [
{
"type": "tags",
"id": "strength"
}
]
}
Response
HTTP/1.1 204 No Content
vary: Accept

A DELETE removes the second set and its file under data/vault/workout_sets/, and answers 204 with no body.

Request
DELETE /api/workout-sets/w-2026-06-03-squat-2 HTTP/1.1
Accept: application/vnd.api+json
Response
HTTP/1.1 204 No Content
vary: Accept

Every error is an errors[] document with status, code, title and detail, plus source when one part of the request is to blame.

With IdStrategy::Provided, a create that carries no id is 400, coded from the example’s WorkoutSetIdRequired error variant:

Request
POST /api/workout-sets HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
"data": {
"type": "workout-sets",
"attributes": {
"set_number": 3,
"weight_grams": 100000,
"reps": 5
},
"relationships": {
"workout": {
"data": {
"type": "workouts",
"id": "w-2026-06-03"
}
},
"exercise": {
"data": {
"type": "exercises",
"id": "back-squat"
}
}
}
}
}
Response
HTTP/1.1 400 Bad Request
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"errors": [
{
"status": "400",
"code": "workout_set_id_required",
"title": "Bad Request",
"detail": "id required: this store requires the caller to supply an id"
}
]
}

A client id that is already taken is 409, and source.pointer points at /data/id:

Request
POST /api/workouts HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
"data": {
"type": "workouts",
"id": "w-2026-06-01",
"attributes": {
"date": "2026-06-01",
"created_at": "2026-06-01T07:30:00Z"
}
}
}
Response
HTTP/1.1 409 Conflict
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"errors": [
{
"status": "409",
"code": "workout_already_exists",
"title": "Conflict",
"detail": "already exists: w-2026-06-01",
"source": {
"pointer": "/data/id"
}
}
]
}

A set’s workout is a String, not an Option, so the relationship cannot be emptied. That is 403, as JSON:API requires for an update the server refuses:

Request
PATCH /api/workout-sets/w-2026-06-03-squat-1/relationships/workout HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
"data": null
}
Response
HTTP/1.1 403 Forbidden
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"errors": [
{
"status": "403",
"code": "relationship_required",
"title": "Forbidden",
"detail": "this relationship cannot be emptied",
"source": {
"pointer": "/data"
}
}
]
}

The lists are not paginated, so page[…] is an unknown query parameter here and the request is 400:

Request
GET /api/workouts?page[limit]=1 HTTP/1.1
Accept: application/vnd.api+json
Response
HTTP/1.1 400 Bad Request
content-type: application/vnd.api+json
vary: Accept
{
"jsonapi": {
"version": "1.1"
},
"errors": [
{
"status": "400",
"code": "invalid_query_parameter",
"title": "Bad Request",
"detail": "`page[limit]` is not a query parameter of this route",
"source": {
"parameter": "page[limit]"
}
}
]
}