The Nuvio API spec
Nuvio publishes its public API as prose markdown, not as a machine-readable document. Two scripts bridge the gap, and a daily CI job watches for drift.
The two files
src/lib/nuvio/nuvio-public-api.snapshot.md— a verbatim copy of the upstream page. It is in.prettierignoreand excluded from the typo check: formatting it would make the drift check diff our own line wrapping forever, and upstream's typos are upstream's.src/lib/nuvio/nuvio-public-api.json— an OpenAPI 3.1 document generated from that snapshot.
Drift checking
bun run nuvio:check # compare the live spec to the snapshot
bun run nuvio:check:accept # accept a new spec, regenerate the JSONscripts/check-nuvio-spec.ts fetches the live spec and compares it. It fails
only when the generated OpenAPI document moves. A reworded or rewrapped page
is reported as a notice and left green — an "API drifted" issue raised over a
reflowed paragraph is how a drift check stops being believed.
CI runs this daily and opens an issue when it fails.
Generating
bun run nuvio:spec # regenerate the JSON from the snapshot
bun run nuvio:spec:check # fail if the committed JSON is stalescripts/build-nuvio-spec.ts parses the snapshot into OpenAPI. It is a real
parser, not an LLM: request blocks become operations, the JSON examples give
each payload its shape, and the field and parameter tables supply types,
nullability, defaults, descriptions and which fields are required.
The parser lives in scripts/nuvio-spec/ and is unit-tested. CI runs
nuvio:spec:check on every PR, so a stale committed JSON fails the build.
The generated types
bun run nuvio:spec writes a third file next to the JSON:
src/lib/nuvio/nuvio-public-api.types.ts, one request / response type pair per
operation (getLibraryDelta → GetLibraryDeltaRequest,
GetLibraryDeltaResponse) plus a NuvioOperations map. nuvio:spec:check
fails when it is stale, the same as for the JSON, and Biome leaves it alone so
the byte-for-byte check holds.
Two things the prose spec only says in words are recovered on the way:
- Enums. A field whose description is nothing but a list of values ("
movieorseries") gets thatenum, and so does every string field of the same name elsewhere in the document : the spec describescontent_typeon the library push but not on the delta feed. "e.g." lists are examples and stay plain strings. - Which fields are there. Request fields follow the tables' "required" column; response fields are all present, because response schemas are built from what the API's examples actually return (nullability included).
types.ts, derived
src/lib/nuvio/types.ts no longer describes the API by hand. Most wire types
are plain aliases of the generated ones (LibraryItem, the three delta events,
Profile, Addon, …), and even ContentType and PosterShape are read off
them. The rest are the generated type with named fields overridden, for one of
three reasons, each said where it happens:
- the spec's table under-types what its prose allows (
avatar_id: null, anullprogress cursor, the health-checkstatusvalues); - the spec's example is the only evidence and is too specific (a settings blob
typed as one client's settings) or too empty (
[]for a supporter list); - the app is stricter than the API on what it sends (it always names the profile rather than lean on the server's default of profile 1).
A SpecContract tuple at the bottom of types.ts checks each override against
the generated type in the direction that matters : what the app sends is a
request the API accepts, what the API returns fits what the app reads. When the
API changes under one of them, bun run check fails there.
Workflow when the API changes
- The daily job fails, or
bun run nuvio:checkfails locally. bun run nuvio:check:acceptupdates the snapshot and regenerates the JSON.- Diff the regenerated JSON — that is the real change.
bun run check. The generated types moved with the spec, so any call site orSpecContractentry the change breaks is now a type error; fix those, andclient.tswhere a route itself changed.- Commit all of it together, so the snapshot and the client never disagree in
main.
This guide lives in the project repo: edit it there, and this page follows within a day.