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:
cd examples/iron-log-mdPORT=39101 cargo run # without PORT it serves on 3001Every 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
- Create a workout
- Create a set that belongs to it
- List workouts, newest first
- One set with its workout and exercise
- Update attributes
- Read a related resource
- Remove from a relationship
- Delete a set
- Errors
Create a workout
Section titled “Create a workout”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.
POST /api/workouts HTTP/1.1Accept: application/vnd.api+jsonContent-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" } ] } } }}HTTP/1.1 201 Createdcontent-type: application/vnd.api+jsonvary: Acceptlocation: /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" } }}Create a set that belongs to it
Section titled “Create a set that belongs to it”A workout set has two to-one relationships, workout and exercise. Both targets are checked to exist before the write.
POST /api/workout-sets HTTP/1.1Accept: application/vnd.api+jsonContent-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" } } } }}HTTP/1.1 201 Createdcontent-type: application/vnd.api+jsonvary: Acceptlocation: /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.
List workouts, newest first
Section titled “List workouts, newest first”sort=-date sorts descending. The list is not paginated, so the document has no meta and no pagination links, only self.
GET /api/workouts?sort=-date HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: 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" } } ]}One set with its workout and exercise
Section titled “One set with its workout and exercise”include=workout,exercise adds both to-one targets to included.
GET /api/workout-sets/w-2026-06-03-squat-1?include=workout,exercise HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: 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" } } ]}Update attributes
Section titled “Update attributes”A PATCH sets only the members it names; rpe goes from null to 8.
PATCH /api/workout-sets/w-2026-06-03-squat-1 HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": { "type": "workout-sets", "id": "w-2026-06-03-squat-1", "attributes": { "reps": 6, "rpe": 8 } }}HTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: 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" } }}Read a related resource
Section titled “Read a related resource”The related endpoint for a to-one relationship answers with the target resource itself.
GET /api/workout-sets/w-2026-06-03-squat-1/exercise HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: 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" } }}Remove from a relationship
Section titled “Remove from a relationship”DELETE on a to-many relationship endpoint removes the identifiers it lists and answers 204. The seeded workout’s file loses its [[strength]] wikilink.
DELETE /api/workouts/w-2026-06-01/relationships/tags HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": [ { "type": "tags", "id": "strength" } ]}HTTP/1.1 204 No Contentvary: AcceptDelete a set
Section titled “Delete a set”A DELETE removes the second set and its file under data/vault/workout_sets/, and answers 204 with no body.
DELETE /api/workout-sets/w-2026-06-03-squat-2 HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 204 No Contentvary: AcceptErrors
Section titled “Errors”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:
POST /api/workout-sets HTTP/1.1Accept: application/vnd.api+jsonContent-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" } } } }}HTTP/1.1 400 Bad Requestcontent-type: application/vnd.api+jsonvary: 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:
POST /api/workouts HTTP/1.1Accept: application/vnd.api+jsonContent-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" } }}HTTP/1.1 409 Conflictcontent-type: application/vnd.api+jsonvary: 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:
PATCH /api/workout-sets/w-2026-06-03-squat-1/relationships/workout HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": null}HTTP/1.1 403 Forbiddencontent-type: application/vnd.api+jsonvary: 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:
GET /api/workouts?page[limit]=1 HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 400 Bad Requestcontent-type: application/vnd.api+jsonvary: 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]" } } ]}