From 2cb0fdcdedc2ac42dd357e5d30a4fd82cb5dabea Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 3 Jun 2026 20:05:08 +0200 Subject: [PATCH] docs(skills): note v2 query params must be direct fields on the handler input A shared/embedded query-param helper struct silently fails to bind under Huma when combined with other query params (found implementing Project's expand); each query param must be a direct field on the operation's input struct. --- .claude/skills/api-v2-routes/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/skills/api-v2-routes/SKILL.md b/.claude/skills/api-v2-routes/SKILL.md index be0d46451..9301b2e4f 100644 --- a/.claude/skills/api-v2-routes/SKILL.md +++ b/.claude/skills/api-v2-routes/SKILL.md @@ -80,6 +80,7 @@ Every handler: pull auth with `authFromCtx(ctx)`, call the matching `handler.Do* } return &fooListBody{Body: NewPaginated(items, total, in.Page, in.PerPage)}, nil ``` +- **Extra query params go *directly* on the handler's input struct — not in a shared/embedded helper.** Beyond `ListParams`, if an operation needs its own query params (`expand`, `order_by`, `include_public`, …), declare each as a direct field with its own `query:"…"` tag on that operation's input struct, then bind it onto the model. A shared or embedded struct of query fields silently **fails to bind** under Huma when combined with other query params/embeds — the field arrives empty (hit while implementing Project's `expand`). Flatten them into the input struct. - **Read** embeds `conditional.Params` in its input, builds an ETag from `id + Updated.UnixNano()`, calls `in.PreconditionFailed(etag, label.Updated)` when `in.HasConditionalParams()`, and returns `*singleReadBody[Model]` with the **quoted** ETag (`"`+etag+`"`). - **Create / Update** take a `Body Model` input and return `*singleBody[Model]`. Update sets `in.Body.ID = in.ID` (URL wins over body). - **Delete** returns `*emptyBody`.