Skip to content

feat(api): add the Folio client, its models and its environment - #6

Merged
DutchyD merged 1 commit into
developmentfrom
feat/folio-api-client
Aug 10, 2026
Merged

feat(api): add the Folio client, its models and its environment#6
DutchyD merged 1 commit into
developmentfrom
feat/folio-api-client

Conversation

@DutchyD

Copy link
Copy Markdown
Contributor

The data layer the site reads Folio through: generated types and schemas, a client, an error taxonomy, and one environment variable. Nothing renders yet — no routes, no components.

Shape

src/env.ts FOLIO_API_URL, parsed at boot, server-only
src/api/generated/ types + Zod from Folio's published contract
src/api/model.ts Site, Project, ProjectSummary, Section, Tag, …
src/api/client.ts createFolioClient({ baseUrl, fetch })
src/api/errors.ts FolioError and its failure taxonomy
src/api/provenance.ts RFC 6901 pointer lookup
src/api/server.ts getSite / getProjects / getProject / getDiagnostics

pnpm api:generate regenerates from https://raw.githubusercontent.com/CodeByDylan/Folio/main/docs/openapi.json. Generation is a deliberate act whose output is committed, so the build never reaches the network and a contract change arrives as a reviewable diff.

Decisions

Model types are inferred from the schemas that parse the responses, not from the parallel generated types. Site is z.infer<typeof zGetSiteResponse>. One definition, so a validator and a type cannot drift apart.

Reads happen on the server only. Server functions wrap the client, so the API origin never enters the browser bundle, CORS is never involved, and parsing costs nothing in shipped bytes. env.ts carries the server-only marker, making a leak a build error rather than a runtime one.

The client holds no reference to the environment. It takes baseUrl and fetch, which is what allowed it to be exercised against a live Folio with nothing configured. Composition lives in server.ts.

Every response is parsed at the boundary. Generated types describe the contract Folio publishes; the running service is a separate artifact that can disagree with it. Parsing is what turns that disagreement into a named error instead of an undefined three components deep.

Failures are reported, not decided.FolioError carries a category — invalid, unauthorized, not-found, unavailable, transport, contract — and the API's own problem details. Nothing maps 400 onto a 404; that is route policy, mirroring Folio's own division between stating facts and setting policy.

Slug format is not re-validated here. Folio already rejects a malformed slug with a message, and duplicating the rule would give it two homes.

Verified against a running Folio

CaseResult
diagnostics()parses
site() before the first buildunavailable (503), "The portfolio has not been built yet."
diagnostics({severity: 'nope'})invalid (400), "'nope' is not a severity."
unreachable hosttransport
pointer escaping/a~1b/c~0d per RFC 6901

That exercise found two real bugs, both since fixed: a nullable-reference bug in Folio's own schema transformer (CodeByDylan/Folio#41), and an over-strict datetime in the generated Zod — Folio emits +00:00, which format: date-time permits, so dates: { offset: true } is set on the generator.

Known limitation

getDiagnostics throws a contract error against a live API until CodeByDylan/Folio#41 merges. The committed output is faithful to what main publishes today, which still marks two omitted properties as required. Run pnpm api:generate after that merge; the diff is two properties. The three content endpoints are unaffected.

Checks

pnpm check, pnpm typecheck and pnpm build pass. Generated output is excluded from Biome, as routeTree.gen.ts already is.

Types and Zod schemas are generated from the contract Folio publishes, so
the shapes cannot be hand-maintained into disagreeing with it. Model types
are inferred from the schemas that parse the responses, leaving one
definition rather than a type and a validator that can drift apart.
Reads go through the server only. The client takes its base URL and fetch,
holding no reference to the environment, so it is exercised without one.
Every response is parsed at the boundary, which is where a deployment that
disagrees with the published contract becomes a named error instead of an
undefined three components deep.
Failures are reported, not decided: a category and the API's own problem
details, leaving the route to choose between a 404, a retry and an error.
@DutchyD
DutchyD merged commit 44b3d49 into developmentAug 10, 2026
2 checks passed
@DutchyD
DutchyD deleted the feat/folio-api-client branch August 10, 2026 11:05
Sign up for freeto 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.

1 participant

@DutchyD