Architectural invariants (do not violate)
- Business logic lives only in Go under
/internal. Electron renderer just consumes the generated client. - Request/response TS types come only from
electron/src/api/schema.ts(auto-generated fromopenapi.yaml). Never hand-write them. - Endpoints must be registered through Huma's
huma.Register(api, op, handler). Barenet/httphandlers never appear in OpenAPI and are invisible to TS.
Workflow
Define the Go types and handler in
internal/api/api.go(or a sub-file in the same package).- Input struct: path/query/header tags drive OpenAPI parameters.
- Output struct must wrap the JSON body in a
Bodyfield (Huma convention). - Annotate fields with
json:"...",example:"...",doc:"..."so the generated TS is self-documenting. - Keep the handler thin; push real logic into a sub-package of
/internal.
Register the operation in
api.Register(api huma.API):huma.Register(api, huma.Operation{ OperationID: "kebab-case-id", // becomes the TS operation key Method: http.MethodGet, // or Post/Put/Patch/Delete Path: "/things/{id}", Summary: "One-line summary", Description: "Longer prose.", Tags: []string{"domain"}, }, handlerFn)Regenerate the contract:
task openapi # Go reflects its routes into openapi.yaml task codegen # openapi-typescript writes electron/src/api/schema.tstask devandtask buildchain these automatically; running them individually is only needed for a one-shot check.Consume from the renderer through
getClient()inelectron/src/api/client.ts. The client isopenapi-fetchtyped bypathsfromschema.ts:const client = await getClient(); const { data, error } = await client.GET("/things/{id}", { params: { path: { id } }, });The auth header (
X-Relay-Token) is injected by the wrapper. Do not re-add it.Verify with
task verifybefore finishing. See theverifyskill if it fails.
Quick reference: existing example
internal/api/api.go already contains get-health and greet operations — mirror their shape rather than inventing a new style. Keep handler signatures func(ctx, *In) (*Out, error).
Things that will silently break the contract
- Returning a plain struct without
Bodywrapper → OpenAPI shows no response schema. - Using
http.HandleFuncon the mux directly → endpoint works at runtime but TS client has no type for it. - Editing
electron/src/api/schema.tsby hand → nexttask codegenoverwrites it; Biome already ignores this file. - Forgetting
task codegenaftertask openapi→ silent type drift between Go and TS.
Definition of done
- New operation visible in
openapi.yaml(diff shows the route underpaths:). - New entry visible in
electron/src/api/schema.tsunderpaths. - Renderer code that calls it typechecks (
task typecheck). task verifypasses.