GymApp SaaS API · v1
Small backend that fetches the free-exercise-db catalog (yuhonas/free-exercise-db, public domain), normalizes it into the app's domain shape, and serves it with low latency. Public read endpoints, plus an admin sync endpoint guarded by a bearer token.
GET/v1/healthz
Liveness probe. Returns 200 OK when the server is up.
{ "ok": true }
curl http://localhost:8787/v1/healthz
GET/v1/exercises
Paginated list of exercises. Sorted by group then name. Gzipped, with an ETag for conditional requests.
description here. The instructions were
63% of this payload (934 KB → 347 KB without them) and only the detail
view ever read them. Get them from
GET /v1/exercises/:id, one
exercise at a time.
| Name | Type | Default | Description |
|---|---|---|---|
limit | integer | 1000 | Max items returned. Capped at 2000. |
offset | integer | 0 | Offset into the sorted list. |
{
"count": 800,
"results": [
{
"id": "free-exercise-db:Barbell_Squat",
"name": "Barbell Squat",
"imageUrl": "https://raw.githubusercontent.com/yuhonas/free-exercise-db/main/exercises/Barbell_Squat/0.jpg",
"group": "Quadriceps",
"muscles": ["Quadriceps", "Calves", "Glutes", "Hamstrings", "Lower Back"],
"muscleSlugs": ["quadriceps"],
"muscleSlugsSecondary": ["calves", "gluteal", "hamstring", "lower-back"],
"equipment": ["barbell"],
"category": "Strength",
"level": "Beginner"
}
]
}
muscleSlugs and
muscleSlugsSecondary are normalized to the
react-native-body-highlighter taxonomy, derived at sync
time from the source dataset's primaryMuscles and
secondaryMuscles lists.
| Header | Value | Notes |
|---|---|---|
Content-Encoding | gzip | When the request sends Accept-Encoding: gzip. 347 KB → ~24 KB. |
ETag | W/"s2-c873-u…-l1000-o0" | Weak. Derived from the list shape version, the row count, the newest updated_at and the paging params — not a hash of the body. |
Cache-Control | public, max-age=0, must-revalidate | The catalog is public, shared data. Revalidation costs a 304, not a download. |
Send the ETag back as If-None-Match and an unchanged
catalog answers 304 Not Modified with no body, before the
list query runs. Weak comparison, so the W/ prefix is
optional and * matches. A re-sync changes
updated_at and therefore the ETag, which is what
invalidates a client's cached copy.
curl 'http://localhost:8787/v1/exercises?limit=2'
# conditional: prints 304
ETAG=$(curl -sI 'http://localhost:8787/v1/exercises' | grep -i '^etag:' | cut -d' ' -f2-)
curl -s -o /dev/null -w '%{http_code}\n' -H "If-None-Match: $ETAG" \
'http://localhost:8787/v1/exercises'
GET/v1/exercises/:id
Single exercise by id. IDs are composite <source>:<sourceId> (e.g. free-exercise-db:Barbell_Squat). This is the only route that returns description. Served with Cache-Control: public, max-age=300.
{
"id": "free-exercise-db:Barbell_Squat",
"name": "Barbell Squat",
"description": "...",
"imageUrl": "https://raw.githubusercontent.com/yuhonas/free-exercise-db/main/exercises/Barbell_Squat/0.jpg",
"group": "Quadriceps",
"muscles": ["Quadriceps", "Calves", "Glutes", "Hamstrings", "Lower Back"],
"muscleSlugs": ["quadriceps"],
"muscleSlugsSecondary": ["calves", "gluteal", "hamstring", "lower-back"],
"equipment": ["barbell"],
"category": "Strength",
"level": "Beginner"
}
{ "error": "not_found" }
curl http://localhost:8787/v1/exercises/free-exercise-db:Barbell_Squat
POST/v1/admin/sync
Force a fresh sync from free-exercise-db. Upserts every catalog entry and recomputes muscle slugs from the source data.
SAAS_ADMIN_TOKEN is set.| Name | Value |
|---|---|
Authorization | Bearer <SAAS_ADMIN_TOKEN> |
{
"totalSeen": 800,
"inserted": 0,
"updated": 800,
"skipped": 0,
"durationMs": 6800
}
{ "error": "unauthorised" }
{ "error": "not_found" }
curl -X POST -H 'Authorization: Bearer <your-token>' \
http://localhost:8787/v1/admin/sync