Skip to content

Generate an OpenAPI document for the node API - #68

Merged
Purple10101 merged 2 commits into
mainfrom
feat/openapi-spec
Sep 23, 2026
Merged

Purple10101 merged 2 commits into
mainfrom
feat/openapi-spec

Conversation

@Babissimo

@Babissimo Babissimo commented Sep 18, 2026 •

Copy link
Copy Markdown

Why

We're putting a browsable API reference at https://api.retina.fm (Scalar, the same three-column layout as ClickUp's docs), with one OpenAPI document per service: retina-server, tower-finder-service and blah2's node API. retina-server and tower-finder are FastAPI and generate theirs for free; Express carries no schema, so blah2's has to be generated from server.js.

This PR generates it here, where the routes live, rather than from outside the repo, so it stays yours to shape and can't drift from the code without CI saying so. api.retina.fm will read api/openapi.json from main via jsDelivr once this lands.

What

  • api/openapi.js runs swagger-autogen (MIT, pinned 2.23.7 as a devDependency) over server.js and adds the info block, the node-local server (http://{node}:3000, defaulting to localhost) and a tag per route family (api, stash, capture, health). A route under a new first path segment fails generation until it gets a tag.
  • api/openapi.json is the committed output: 23 operations on 20 paths, deterministic byte for byte.
  • npm run openapi regenerates; npm run openapi:check exits 1 when the committed copy is stale.
  • .github/workflows/api.yml runs the check on PRs touching api/** and on pushes to main. No existing workflow ran anything for api/.
  • api/package-lock.json is committed (it was gitignored) and both CI and the image install with npm ci. A byte-exact check needs the generator's whole dependency tree fixed, not just swagger-autogen. For the image this also pins express (4.22.3) and js-yaml (4.3.2), which until now resolved afresh on every build.
  • api/Dockerfile installs with npm ci --omit=dev, so swagger-autogen (and 13 transitive packages, about 2.3 MB) stays out of the image.
  • README: one line on regenerating.

server.js is untouched. Summaries, descriptions and request/response schemas can be added per route later with #swagger comments, which the generator picks up; that's your call.

Worth knowing

  • The IPC routes are in the spec. POST /capture/retune/ack, /reject, /overload-status and /rf-status are the blah2 core talking to this API. Like POST /capture/retune they are unauthenticated on all interfaces, so anyone on the node's LAN can already post to them; the code is public, so documenting them discloses nothing new, but it does make them easier to find. The Cloudflare tunnel route in node-infra also covers /capture, behind Access.
  • No response bodies are documented, so the reference is a route map. Filling them in means #swagger comments in server.js. The JSON routes go out as text/html because res.send is handed a string, so a schema written against today's responses would have to say text/html.
  • Other gaps swagger-autogen can't see: the plain-text CSV bodies on the IPC POSTs, the "any" placeholders on the retune JSON fields, and the empty default response on GET /capture/retune. All fixable with #swagger comments.
  • Pre-existing, not touched here: the compose file's build: ./api doesn't match the Dockerfile's COPY api/... paths (which need the repo root as context, as CI uses), and .dockerignore doesn't exclude api/node_modules, so a local build copies a developer's node_modules into the image.

Verified

  • npm ci && npm run openapi:check passes on a clean git archive in node:20-slim (npm 10.8.2), and on Node 26 locally.
  • The check fails on drift (a new /api route; a route under an untagged segment) and passes on a comment-only change to server.js.
  • The tag handling in openapi.js keeps the curated tag list if swagger-autogen emits a top-level tags, and falls back on tags: []; checked against a stubbed generator.
  • All 8 api/test_*.js pass.
  • The document passes openapi-spec-validator; actionlint is clean on the workflow.

ClickUp: API reference at api.retina.fm

🤖 Generated with Claude Code

@Purple10101
Purple10101 self-requested a review September 18, 2026 13:39

@Purple10101 Purple10101 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this out by hand and with Claude.

Verified against my desk node (blah2-api v0.5.0): the route table in the running
container matches the spec 24/24, and all 19 documented GETs return 200.

That said, no operation documents a response body, so what we publish is an accurate
route map rather than a reference. The types are as follows if we want to detail them:

Route Content-Type Body
/api/timestamp text/html 1789753049651
/api/tracker text/html {"timestamp":...,"n":7,"nTentative":7,...}
/api/detection text/html {"timestamp":...,"delay":[],"doppler":[],...}
/api/map, /api/timing, /api/iqdata text/html all JSON

I am not sure we need to do this at all, but it is something I want to make sure is
deliberate?

One ask before merge

The workflow runs npm install with api/package-lock.json gitignored.
swagger-autogen is pinned, but acorn, deepmerge, glob and json5 resolve by
caret range on every run. Pairing a byte-exact comparison with a floating dependency
tree means a patch release upstream can turn every api/** PR red with no code change
here.

Could we commit the lockfile and switch to npm ci?

Two optional one-liners while you're in there

  • ...rest on api/openapi.js:105 is spread after tags, so it would silently clobber
    the curated tag list if swagger-autogen ever emitted a top-level tags key.
  • operation.tags || [tagFor(route)] on api/openapi.js:78 skips the fallback on an
    empty array.

Both are inert today.

The RETINA API reference at api.retina.fm shows blah2's node-local API
next to retina-server's and tower-finder-service's, reading it from
api/openapi.json on main. Express carries no schema, so the document is
generated from the routes in server.js, where the code lives:
api/openapi.js runs swagger-autogen (a pinned devDependency) over it and
adds the info block, the node-local server address and a tag per route
family.

CI regenerates it on every PR that touches api/ and fails when the
committed copy differs, so the reference stays current with the code.
A byte-exact check needs the generator's whole dependency tree fixed,
not only swagger-autogen, or an upstream patch release could fail every
api/ PR with no change here. So api/package-lock.json is committed
rather than ignored, and CI and the image both install from it with
npm ci.

swagger-autogen is needed only to generate the document, so the image
installs with --omit=dev rather than shipping it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Babissimo

Copy link
Copy Markdown
Author

Rebased onto main (the tracker route went in #69, so the spec is now 23 operations on 20 paths) and amended:

  • api/package-lock.json is committed and CI installs with npm ci. The Dockerfile does too (npm ci --omit=dev), so the image's express and js-yaml are pinned by the same lockfile rather than resolved per build; say if you'd rather keep the image on npm install.
  • tags is destructured out alongside info and servers, so ...rest can't replace the curated list, and the tag fallback now covers an empty array. Checked both against a stubbed generator; output unchanged.

On response bodies: deliberate. This PR is the route map, and filling bodies in means #swagger comments in server.js, which I've left to you. If you do, note those routes send JSON as text/html because res.send gets a string; res.type('json') first would let the reference say application/json.

/api/tracker went out in #69, but the api tag description still listed
"tracks" among what the tag covers, so the reference advertised a route
that no longer exists.

openapi:check cannot catch this: the tag text is hand-written and lands
identically on both sides of the comparison, so it stays green while the
prose goes stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Purple10101
Purple10101 dismissed their stale review September 23, 2026 13:38

I ended up taking this PR due to some merge conflicts that I had the best context to resolve.

@Purple10101
Purple10101 merged commit 52af027 into main Sep 23, 2026
2 checks passed
@Purple10101
Purple10101 deleted the feat/openapi-spec branch September 23, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants