Scaffold a form
Build $form with React Hook Form validated against the feature's existing zod
request contract. The form owns input state; a mutation hook owns submission.
Step 1 — Locate the contract and feature
Find the owning feature and its request schema (CreateXBody, UpdateXBody). If
no contract exists yet, stop and run /add-api-contract first — the form
validates against that schema, it does not define one.
Step 2 — Ensure dependencies
Check for react-hook-form and @hookform/resolvers. Install only what is
missing through the detected package manager. Reuse the project's existing field
primitives in shared/ui (Field, Input, FormError) before adding new ones.
Step 3 — Build the form
Apply references/form-patterns.md.
- Type
useFormasz.inferof the request schema; resolve withzodResolver(<RequestBody>). - Derive
defaultValuesfrom typed data (edit) or schema-shaped literals (create). - Register inputs against schema field names; render
formState.errorstied to inputs witharia-invalid/aria-describedby.
Step 4 — Wire submission through a mutation
Call the feature's TanStack Query mutation hook (useCreateX / useUpdateX); do
not fetch in the component. Disable submit with the mutation's isPending. Map
server failures back onto fields with setError, or to a form-level message.
Step 5 — Verify
source "${CLAUDE_PROJECT_DIR}/.claude/lib/detect-toolchain.sh"
run_typecheck
run_tests
Confirm the resolver schema is the same one the API parses, and no parallel form
schema or interface was introduced.
Checklist
- Resolver uses the feature's existing zod request schema; values are its
z.infer. - No second form schema or parallel
interface. - Submission goes through a
useMutationhook; nofetch/invalidation in the form. - Submit disabled via
isPending; field and server errors surfaced accessibly. - Generic field primitives live in
shared/ui; the domain form stays in its feature. -
run_typecheckandrun_testspass.