Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Forms

ASecurity

Forms with React Hook Form + Zod. Use this skill whenever the user is building a form, adding a form field, writing a Zod schema for a form, creating a form hook, wiring a Server Action to a form, or handling form validation. Trigger when the user creates a new form component, asks how to validate a field, wants to handle form submission, or needs to show server-side errors. Do NOT trigger for general Zod schema questions unrelated to forms.

5 stars
0 votes
0 copies
0 views
Added 9/23/2026
ai-agentsrustreact

Works with

cli

Security Analysis

A100/100

Scanned 9/23/2026

Install to Claude Code

$npx -y skills add JoseCortezz25/template-starter-nextjs --skill forms --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Forms?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Forms
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/josecortezz25-forms/badge)](https://www.skillsdirectory.com/skills/josecortezz25-forms)

More formats (shields.io, HTML) on the badges page.

Download with Pro
Files
SKILL.md
---
name: forms
description: Forms with React Hook Form + Zod. Use this skill whenever the user is building a form, adding a form field, writing a Zod schema for a form, creating a form hook, wiring a Server Action to a form, or handling form validation. Trigger when the user creates a new form component, asks how to validate a field, wants to handle form submission, or needs to show server-side errors. Do NOT trigger for general Zod schema questions unrelated to forms.
---

# Forms — React Hook Form + Zod

**Every form in this project MUST use React Hook Form + Zod. No exceptions.**

---

## Required Stack

| Concern                 | Tool                      | Location             |
| ----------------------- | ------------------------- | -------------------- |
| Form state & submission | React Hook Form           | Custom hook          |
| Validation schema       | Zod                       | `[entity].schema.ts` |
| Schema → RHF bridge     | `@hookform/resolvers/zod` | Custom hook          |
| Error messages          | `validation-messages.ts`  | Per domain           |
| Form submission         | Server Action             | `actions.ts`         |

---

## File Structure (per form)

```
domains/[entity]/
├── [form-name].schema.ts          ← Zod schema + inferred type
├── hooks/
│   └── use-[form-name]-submit.ts  ← useForm, resolver, onSubmit
├── components/organisms/
│   └── [form-name]-form.tsx       ← purely presentational
└── actions.ts                     ← Server Action with server-side revalidation
```

---

## 1. Schema (`[form-name].schema.ts`)

```tsx
// domains/auth/login.schema.ts
import { z } from 'zod';
import { authValidationMessages } from './validation-messages';

export const loginSchema = z.object({
  email: z.string().email(authValidationMessages.email),
  password: z.string().min(8, authValidationMessages.passwordTooShort)
});

export type LoginInput = z.infer<typeof loginSchema>;
```

Rules:

- One schema per file — never mix unrelated schemas
- Export both the schema constant and the inferred `Input` type
- Error messages from `validation-messages.ts` — no hardcoded strings
- Never use `any` or bypass `.safeParse()` type narrowing

---

## 2. Hook (`use-[form-name]-submit.ts`)

```tsx
// domains/auth/hooks/use-login-submit.ts
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema, type LoginInput } from '../login.schema';
import { loginAction } from '../actions';

export function useLoginSubmit() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError
  } = useForm<LoginInput>({
    resolver: zodResolver(loginSchema)
  });

  const onSubmit = handleSubmit(async data => {
    const result = await loginAction(data);
    if (result?.error) {
      setError('root', { message: result.error });
    }
  });

  return { register, onSubmit, errors, isSubmitting };
}
```

Rules:

- Always pass `resolver: zodResolver(schema)` to `useForm`
- Type `useForm<SchemaInput>` explicitly — never infer `any`
- Wrap submission with `handleSubmit` — never call directly
- Map server errors back via `setError('root', ...)`
- Never call Server Actions directly from a component

---

## 3. Form Component (`[form-name]-form.tsx`)

```tsx
// domains/auth/components/organisms/login-form.tsx
'use client';
import { useLoginSubmit } from '../../hooks/use-login-submit';
import { authMessages } from '../../messages';

export function LoginForm() {
  const { register, onSubmit, errors, isSubmitting } = useLoginSubmit();

  return (
    <form onSubmit={onSubmit} className="login-form">
      <div className="login-form__field">
        <label htmlFor="email">{authMessages.login.emailLabel}</label>
        <input id="email" type="email" {...register('email')} />
        {errors.email && (
          <span className="login-form__error">{errors.email.message}</span>
        )}
      </div>

      <div className="login-form__field">
        <label htmlFor="password">{authMessages.login.passwordLabel}</label>
        <input id="password" type="password" {...register('password')} />
        {errors.password && (
          <span className="login-form__error">{errors.password.message}</span>
        )}
      </div>

      {errors.root && (
        <p className="login-form__root-error">{errors.root.message}</p>
      )}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting
          ? authMessages.login.submitting
          : authMessages.login.submit}
      </button>
    </form>
  );
}
```

Rules:

- Form component only renders — all logic lives in the hook
- Use `{...register('fieldName')}` for every input
- Always render `errors.{field}.message` beside each field
- Always handle `errors.root` for server-side errors
- Disable submit button while `isSubmitting`
- Never use `useState` to track field values — RHF owns state
- Never use `e.preventDefault()` — `handleSubmit` does it

---

## 4. Server Action (`actions.ts`)

```tsx
// domains/auth/actions.ts
'use server';
import { loginSchema, type LoginInput } from './login.schema';

export async function loginAction(input: LoginInput) {
  const validated = loginSchema.safeParse(input);
  if (!validated.success) {
    return { error: 'Invalid input' };
  }

  const { email, password } = validated.data;
  // ... business logic
}
```

Rules:

- Always revalidate on the server with `schema.safeParse()` — never trust client validation alone
- Return `{ error: string }` for errors the hook can pass to `setError('root', ...)`

---

## Multi-Step Forms

Split by step. Compose with `.merge()` for server validation.

```tsx
// step schemas stay independent
export const step1Schema = z.object({
  firstName: z.string().min(1),
  lastName: z.string().min(1)
});
export const step2Schema = z.object({
  role: z.enum(['admin', 'member']),
  team: z.string().min(1)
});

// composed for server-side full validation
export const onboardingSchema = step1Schema.merge(step2Schema);
export type OnboardingInput = z.infer<typeof onboardingSchema>;
```

---

## Anti-Patterns

```tsx
// ❌ useState for form fields
const [email, setEmail] = useState('');

// ❌ Manual validation without Zod
if (!email.includes('@')) { setError('Invalid email'); }

// ❌ Inline schema in component
const schema = z.object({ email: z.string() }); // must be in .schema.ts

// ❌ Server Action directly in JSX for complex forms
<form action={loginAction}>

// ❌ useForm without resolver
useForm(); // validation won't run

// ❌ Hardcoded error messages in schema
z.string().email('Please enter a valid email') // must be in validation-messages.ts
```

---

## Pre-Submit Checklist

- [ ] Schema in `{name}.schema.ts` with exported type
- [ ] Validation messages in `validation-messages.ts`
- [ ] `useForm<InputType>({ resolver: zodResolver(schema) })` in the hook
- [ ] Server Action revalidates with `schema.safeParse(input)`
- [ ] `errors.root` handled for server errors
- [ ] Submit button disabled during `isSubmitting`
- [ ] All field errors rendered beside each input

Attribution

JoseCortezz25JoseCortezz25
View sourceMore from JoseCortezz25 →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Caveman

Ultra-compressed communication mode that cuts output tokens while keeping technical accuracy. Levels: lite, full, ultra and the wenyan variants. Use for /caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".

1066601 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

686011 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3351 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

651 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →