BXUP Track progress & test yourself — free
Frontend & Mobile · Playbook · Foundation · ~4 min read
FRONTEND & MOBILE · PLAYBOOK · FOUNDATION

Scaffold Schema-First Forms with Every State Enumerated

Give the model a Zod schema and a named list of states — error, dirty, disabled, submitting, server-error — and it scaffolds the tedious 70% of form work flawlessly. Withhold the state list and you get the demo-quality happy path that falls over on its first failed submit.

Tier: Foundation · Workflow area: Component development · Tools: react-hook-form + Zod, GitHub Copilot, Claude

Forms are the highest-volume, most state-dense UI work in product engineering, which makes them ideal AI territory — provided you drive generation from a schema, not a vibe. Schema-first means validation, types, and UI all derive from one source of truth, so nothing drifts.

When to use

  • Any form with three or more fieldssettings pages, checkout steps, onboarding flows, admin CRUD.
  • Validation rules are known or knowableyou can write them as a schema before writing any JSX.
  • Server errors are part of realitythe API can reject a submit and the UI must say so, per-field where possible.
  • Repeating a house form patternonce your exemplar form exists (pb-02), each new form is a five-minute scaffold.

When NOT to use

  • The schema is still being negotiatedgenerating UI against a schema that changes daily produces churn; stabilize the contract first.
  • Wizard-of-oz prototypesbehind the throwaway boundary (pb-01), a hardcoded form is fine.
  • Highly bespoke input UXa visual query builder or canvas-based editor isn't a "form"; use the interaction playbook (pb-05).

The recipe

  1. Write the Zod schema first, by hand, with the server's rules in it. This is the step that keeps you in charge:

    export const inviteSchema = z.object({
      email: z.string().email('Enter a valid work email'),
      role: z.enum(['viewer', 'editor', 'admin']),
      teamIds: z.array(z.string().uuid()).min(1, 'Pick at least one team'),
      message: z.string().max(280, 'Keep it under 280 characters').optional(),
    });
    export type InviteInput = z.infer<typeof inviteSchema>;
    
  2. Prompt with the schema plus the full state enumeration. The enumeration is the payload of this playbook — paste it every time (or bake it into your instruction file):

    Scaffold an InviteMemberForm using react-hook-form + zodResolver and the
    attached inviteSchema. Use @acme/ui components only (TextField, Select,
    MultiSelect, Textarea, Button, Banner). Implement EVERY state:
    1. pristine — no errors shown, submit enabled
    2. field error — on blur + on submit, message under the field, input
       gets aria-invalid and aria-describedby pointing at the message
    3. dirty/unsaved — "Discard changes?" guard on navigation
    4. submitting — submit disabled with spinner, all fields disabled
    5. server field-error — map 422 {errors: {email: "..."}} onto the
       matching fields via setError
    6. server global-error — Banner above the form with retry, form stays
       filled
    7. success — call onSuccess(invite), reset the form
    Submit handler calls inviteMember(input): Promise<Result>. No fetch here.
    
  3. Inspect for the three classic scaffold bugs before running anything. A good output shows: mode: 'onBlur', disabled={isSubmitting} on every input and the button, and a setError loop for server errors. The classic bugs to catch: validation firing on every keystroke from first render, the submit button disabled merely because the form is pristine-invalid (traps keyboard users out of discovering errors), and server errors rendered only as a toast that vanishes.

  4. Generate the unhappy-path tests, then hand-verify the two worst ones. Ask for behavior-first tests (see pb-22):

    Write RTL + Vitest tests for InviteMemberForm covering: invalid email
    blur shows the message and wires aria-describedby; submit with server
    422 maps the error to the email field; global 500 shows the Banner and
    preserves entered values; double-click submit fires inviteMember once.
    
    npx vitest run src/features/members/InviteMemberForm.test.tsx
    

    Then verify by hand in the browser: the 422 path and the double-submit path. Mock the failure at the network layer (MSW, pb-07) and click around like an annoyed user.

  5. Wire the schema to the server contract so drift is impossible. If the API has an OpenAPI/GraphQL schema, derive or cross-check the Zod schema from it rather than duplicating rules:

    npx openapi-zod-client ./openapi.json -o src/api/zod-schemas.ts
    

    One schema, three consumers: client validation, form types, request typing.

Note

Accessibility is part of "every state," not a follow-up. Field errors must be programmatically associated (aria-describedby), the global error Banner should be a live region, and focus should move to the first invalid field on a failed submit. Put these in the prompt — generated forms only include them when asked.

Make it yours

  • React (the recipe as written)react-hook-form + @hookform/resolvers/zod; for Next.js server actions, keep the same schema and run inviteSchema.safeParse on the server too — never trust the client copy alone.
  • Vue 3VeeValidate + @vee-validate/zod (or zod + composable): same prompt structure, swap the library names and ask for useForm({ validationSchema: toTypedSchema(inviteSchema) }); the seven-state enumeration transfers unchanged.
  • Angulartyped Reactive Forms; ask the model to generate the FormGroup from the schema shape and a zodValidator bridge, states via form.status, dirty, and a server-error mapper into setErrors.
  • React Nativereact-hook-form works as-is; the state list gains two entries: keyboard-avoiding behavior while erroring, and submit-while-offline (queue or explicit failure).

Quality check

  • All seven states demonstrablewalk the form through each one against MSW mocks; screenshot or story each (pb-06 pairs well).
  • Schema is the single sourceno validation rule exists only in JSX; grep for ad-hoc required attributes or inline regexes.
  • Unhappy-path tests pass and mean somethingthe 422-mapping and double-submit tests fail if you break the handler (try it).
  • Accessibility wiring presentaria-invalid, aria-describedby, focus-on-error verified with a keyboard-only pass.
  • Server re-validatesthe same rules run server-side; the client schema is UX, not security.
Key Takeaway:

The model is excellent at form plumbing and terrible at deciding which states matter — so you own the schema and the seven-state enumeration, it owns the scaffold, and the unhappy paths get hand-verified every single time.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in