Skip to content

Typed API client (openapi-fetch + generated types) and typed Inertia page props (#504 #505) - #576

Merged
antosubash merged 19 commits into
mainfrom
feat/typed-client-b4
Oct 22, 2026
Merged

antosubash merged 19 commits into
mainfrom
feat/typed-client-b4

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Batch 4 adds a shared typed API client generated from the OpenAPI export (#504) and typed Inertia page props (#505). Design: docs/superpowers/specs/2026-10-22-typed-client-props-design.md.

What changes

#504 Typed API client

  • New @simple-module-py/ui client:
    • createApi<paths>(): an openapi-fetch client.
    • apiResult(...) normalises a call's result to {ok, status, data, detail, fieldErrors, error}.
    • apiFetch covers multipart uploads and non-/api routes.
    • useApiErrorToast(fallbackKey) is opt-in. The client itself contains no UI strings.
  • CSRF:
    • New shared prop csrfToken. Only signed-in sessions get it, and it is resolved lazily when an Inertia page renders, so JSON API calls and downloads never create one.
    • The client sends X-CSRF-Token on unsafe methods, and only to same-origin URLs.
    • Server enforcement is unchanged (opt-in RequiresCsrf).
  • Generator:
    • smpy openapi gen-types [--module X] writes a committed <pkg>/pages/api.generated.ts per module.
    • New targets make gen-api-types and ci-api-types-fresh (the latter in make lint and the CI js-typecheck job).
    • *.generated.ts files are exempt from Biome and the 300-line cap.
    • openapi-typescript is a devDependency of packages/ui, because it needs TypeScript 5. Scaffolded apps fall back to npx.
  • Migrated:
    • tenants: 8 calls are now typed and 2 use apiFetch (one calls the settings module's endpoint, the other is a multipart logo upload).
    • api_tokens: 1 call. Its unused api_url prop is removed (a behaviour change, noted in the CHANGELOG).
  • Scaffold: templates ship pages/api.ts plus the generated types, and pages use api.* with typed props. make new-module runs gen-types. Skills and docs are updated.
  • Untyped responses: 15 /api endpoints still have no response type, mostly file downloads and redirects. They are listed in docs/superpowers/specs/2026-10-22-untyped-endpoints.md.

#505 Typed page props

  • New usePageProps<T>() hook.
  • SharedProps (auth, menus, i18n, csrfToken, nativePrefixes) is registered through Inertia's sharedPageProps augmentation.
  • Pages take typed component args.
  • All double casts in shipped code are removed (44, plus 3 new ones that arrived from main during the batch). Three justified casts remain, each marked // cast-ok: <reason>.
  • New check ci-check-no-double-cast, run in make lint and the CI python-lint job. It covers module frontend dirs, packages/ui and host/client_app, and excludes tests.
  • docs/frontend/shared-props.md no longer teaches the cast.

Verification (after merging main with batches 2 and 3)

  • make lint ✓
  • make test-py: 5590 passed ✓
  • make test-js: 708 passed ✓
  • make ci-doctor ✓
  • make ci-api-types-fresh ✓
  • uv lock --check ✓
  • Redis-backed subset: 200 passed ✓

/ship

The rest of the fetch migration is next: users, background_tasks, branding, settings and file_storage, plus a check that rejects raw fetch() in pages. Page props generated from Python DTOs will be a follow-up issue.

Closes #505

Part of #504; it stays open for the rest of the fetch migration.

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

…rorToast (#504)

createApi<paths>() wraps openapi-fetch with same-origin credentials,
Accept: application/json and the csrfToken shared prop as X-CSRF-Token on
unsafe methods. apiResult()/apiFetch() normalise to
{ ok, status, data, detail, fieldErrors, error }, network failures as
status 0. openapi-typescript is pinned as a packages/ui devDependency so
it can use TypeScript 5 without displacing the repo's TypeScript 7.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Anonymous pages (and therefore any page a module may mark cacheable)
never carry it, and nothing is minted into their session. API-token
principals render as anonymous and get none either.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
#504)

Cuts each module's /api operations (and only the component schemas they
reach) out of the in-memory schema and runs openapi-typescript into
<pkg>/pages/api.generated.ts. Without --module it regenerates modules that
already have the file; it never writes outside the project. Operations
whose JSON success body has no schema are listed as untyped.

ci-api-types-fresh hashes the generated files before/after like
ci-i18n-fresh; it runs in make lint and the js-typecheck CI job.
*.generated.ts is exempt from Biome and the 300-line cap.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…504)

tenants: 10 fetch calls move to api.* (8 typed, 2 through apiFetch: the
settings module's route and the server-supplied multipart upload URL);
useTenantErrors.describe takes an ApiResult and keeps each call's own
offline copy. apiDetail.ts is gone. api_tokens: postJson becomes typed
api.POST calls that still pin the page's csrf_token prop; the unused
api_url prop is dropped. Pages take typed component args instead of the
usePage() double cast.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
apiFetch and the createApi middleware attached X-CSRF-Token whatever the
destination, so an absolute or protocol-relative URL (or a foreign
baseUrl) would hand the session's token to that origin. The header is now
set only when the request's origin equals location.origin.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Module templates (base and framework_repo) add pages/api.ts, a sample
pages/api.generated.ts matching what gen-types emits for the CRUD routes
(pinned by test_cli_gen_types_scaffold), generated DTO types for page
props, and a Browse delete action through api.DELETE. make new-module
regenerates the types once the module is installed; smpy create-module
prints the gen-types command (the module is not importable yet there).

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Untyped /api endpoints (JSON success body without a schema), 15 in all:
AuditLog: GET /api/audit_log/export.csv
Branding: GET /api/branding/logo, /logo-dark, /favicon
FileStorage: GET files/{id}/thumbnail, files/{id}/download,
  public/{id}/thumbnail, public/{id}/{filename}, public/{id}
Users: DELETE auth/token; POST auth/forgot-password, auth/reset-password,
  auth/request-verify-token; GET auth/{provider}/login, /callback
Details in docs/superpowers/specs/2026-10-22-untyped-endpoints.md.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-22T11:00:35.001544Z 6cdaf19 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6cdaf19
Status: ✅  Deploy successful!
Preview URL: https://55fd989c.simple-module-python.pages.dev
Branch Preview URL: https://feat-typed-client-b4.simple-module-python.pages.dev

View logs

@antosubash
antosubash merged commit 162072a into main Oct 22, 2026
17 checks passed
@antosubash
antosubash deleted the feat/typed-client-b4 branch October 22, 2026 11:38
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.

Frontend: Inertia page props are hand-typed and force-cast (44 as unknown as)

1 participant