Derive the frontend's API types from the backend's OpenAPI schema - #16
Open
albertotb wants to merge 2 commits into
Open
Derive the frontend's API types from the backend's OpenAPI schema#16albertotb wants to merge 2 commits into
albertotb wants to merge 2 commits into
Conversation
FastAPI already knows the whole contract, so stop restating it by hand:
`make generate-types` dumps the schema (offline, no server needed) and
openapi-typescript writes frontend/src/api/schema.d.ts. The wrappers in
api/backend.ts now read their request and response types out of that
file instead of declaring them, so an endpoint that is renamed or
reshaped upstream breaks compilation instead of failing at runtime.
The Contract workflow gains the matching gate: regenerate and fail if the
committed types differ. It checks `git status`, not `git diff` — the
latter ignores an untracked or deleted file and passes vacuously, which
it did on the first attempt here.
Verified by simulating a sync that renames Response.output to .result:
the drift check reports the diff and tsc fails with "Property 'output'
does not exist on type '{ result: number; }'".
Existing runtime assertions are untouched: types prove shape, not that
the server answers, and nothing here covers the nginx prefix (Docker's
job).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011texLkDBELWbXsBf6San3M
`cd backend && uv sync` on the first line of a run block left the shell in backend/, so `npm --prefix frontend ci` resolved to backend/frontend and failed with "can only install with an existing package-lock.json". Split into two steps with working-directory, which cannot leak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011texLkDBELWbXsBf6San3M
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The "anything better to automate the contract" question, implemented. FastAPI already knows the whole contract, so this stops restating it by hand.
How it works
make generate-typesdumps the schema without booting a server and runs openapi-typescript over it:src/api/backend.tsthen reads its types out of that file instead of declaring them:The hand-mirrored
interface PredictResponse { output: number }is gone — that type now comes from the backend's Pydantic model, keyed by the endpoint path, so renaming the route breaks compilation too.The gate
Contractregenerates and fails if the committed types differ. It checksgit status, notgit diff— the latter ignores an untracked or deleted file and passes vacuously, which is exactly what happened on my first attempt (the check said "no drift" whiletscwas already failing).Proof it earns its keep
I simulated a sync renaming
Response.output→.result:schema.d.ts is stale+ difftsc/ buildProperty 'output' does not exist on type '{ result: number; }'.output == 5)The difference: the curl assertion only catches changes to the three endpoints someone remembered to assert; the types catch any endpoint, parameter or field the frontend touches.
Scope, deliberately
Types-only rather than a generated client (tiangolo's template generates a full SDK with
@hey-api/openapi-ts). One generated file, and the hand-written wrappers stay readable — a template should show the pattern, not hide it. If you'd rather have the full SDK later, hey-api also has a TanStack Query plugin that would fit this stack.Existing checks untouched: types prove shape, not that the server answers (
Contract's runtime assertions) and not that nginx strips/api(Docker). Structural / behavioural / infrastructural, one job each. Slimming the runtime assertions is possible now but I kept them so this PR only adds coverage.Costs, honestly: one generated file committed (inherited by projects made from the template), a
make generate-typesstep to remember locally (the gate catches forgetting), andContractnow installs node as well as uv (~15s).🤖 Generated with Claude Code
https://claude.ai/code/session_011texLkDBELWbXsBf6San3M
Generated by Claude Code