Skip to main content

CLI Codegen

Generate static .tsx form components at build time using @zod-to-form/cli. This guide covers installation, the zodform generate command, generated output structure, component configuration, auto-save mode, server actions, watch mode, and the programmatic API.

When to Use

Use the CLI codegen path when a project needs static, hand-readable .tsx form components generated from Zod v4 schemas at build time. Best suited for production forms, design system integration, and cases where the generated code should be inspected, customized, and committed — with zero runtime dependency on zod-to-form.

Prerequisites

  • Node.js >= 20
  • Zod v4 (zod@^4.0.0) — Zod v3 is not supported
  • A Zod schema file with a named export

Installation

pnpm add -D @zod-to-form/cli zod

The CLI is a dev dependency — it runs at build time, not in production.

Basic Usage

1. Define a Schema File

// src/schemas/user.ts
import { z } from 'zod';

export const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
role: z.enum(['admin', 'editor', 'viewer']),
bio: z.string().optional(),
newsletter: z.boolean().default(false)
});

2. Generate the Form Component

npx zodform generate \
--schema src/schemas/user.ts \
--export userSchema \
--out src/components/ \
--name UserForm

3. Generated Output

The generated src/components/UserForm.tsx imports only react-hook-form, @hookform/resolvers, and the schema — no @zod-to-form/* imports appear:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { userSchema } from '../schemas/user';

type FormData = z.output<typeof userSchema>;

export function UserForm(props: { onSubmit: (data: FormData) => void }) {
const { register, handleSubmit } = useForm<FormData>({
resolver: zodResolver(userSchema)
});

return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<div>
<label htmlFor="name">Name</label>
<input id="name" type="text" {...register('name')} />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...register('email')} />
</div>
<div>
<label htmlFor="role">Role</label>
<select id="role" {...register('role')}>
<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
</div>
<button type="submit">Submit</button>
</form>
);
}

CLI Options

Required Flags

FlagDescription
--schema <path>Path to the TypeScript/JavaScript module containing the Zod schema
--export <name>Named export that contains the z.object(...) schema

Optional Flags

FlagDefaultDescription
--out <path>./<Name>Form.tsxOutput directory or .tsx file path
--name <name>Derived from --exportComponent name. If omitted, derived by stripping Schema suffix and appending Form
--mode <mode>submitsubmit — standard handleSubmit pattern; auto-savewatch + useEffect pattern
--ui <preset>shadcnshadcn or unstyled
--component-config <path>Path to component config file (.json or .ts)
--forcefalseOverwrite existing output file
--dry-runfalsePrint generated code to stdout without writing files
--server-actionfalseGenerate a Next.js server action alongside the form
--watchfalseWatch the schema file and regenerate on changes

Naming Conventions

--export valueDerived --nameOutput file
userSchemaUserFormUserForm.tsx
orderSchemaOrderFormOrderForm.tsx
loginDataLoginDataFormLoginDataForm.tsx

Override with --name:

zodform generate --schema src/user.ts --export userSchema --name ProfileEditor
# ProfileEditor.tsx

Generation Modes

Submit Mode (default)

Generates handleSubmit + onSubmit prop pattern:

npx zodform generate --schema src/schemas/user.ts --export userSchema

Auto-Save Mode

Generates watch + useEffect pattern with onValueChange callback and no submit button:

npx zodform generate --schema src/schemas/user.ts --export userSchema --mode auto-save

Output uses mode: 'onChange' in useForm and fires onValueChange on every field update:

import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

export function UserForm(props: {
onValueChange?: (data: FormData) => void;
onSubmit?: (data: FormData) => void;
}) {
const { register, watch } = useForm<FormData>({
resolver: zodResolver(userSchema),
mode: 'onChange'
});

useEffect(() => {
const subscription = watch((values) => {
props.onValueChange?.(values as FormData);
});
return () => subscription.unsubscribe();
}, [watch, props.onValueChange]);

return <form>{/* fields, no submit button */}</form>;
}

Arrays

When the schema contains z.array(), the output includes useFieldArray:

import { useForm, useFieldArray } from 'react-hook-form';

const { register, handleSubmit, control } = useForm<FormData>({ /* ... */ });
const { fields: itemsFields, append: appendItems, remove: removeItems } = useFieldArray({
control, name: 'items'
});

Component Configuration

Map field types to custom components using a config file. This same format works with the runtime <ZodForm> — see Component Config.

npx zodform generate \
--schema src/schemas/user.ts \
--export userSchema \
--component-config src/config/form-components.ts \
--out src/components/

The generated file will include static imports from the config's components path and apply per-field props:

import { TextInput, TextareaInput } from '@/components/ui';

<TextareaInput id="bio" {...register('bio')} rows={6} />

Server Actions

Generate a paired Next.js server action alongside the form:

npx zodform generate \
--schema src/schemas/user.ts \
--export userSchema \
--server-action \
--out src/components/

Produces both UserForm.tsx and user-form-action.ts.

Watch Mode

Regenerate automatically when the schema file changes:

npx zodform generate \
--schema src/schemas/user.ts \
--export userSchema \
--out src/components/ \
--watch

Combine with --force to overwrite on each regeneration.

Programmatic API

runGenerate(options)

import { runGenerate } from '@zod-to-form/cli';

const result = await runGenerate({
schema: './src/schemas/user.ts',
export: 'userSchema',
out: './src/components/',
name: 'UserForm',
mode: 'submit',
ui: 'shadcn',
force: true,
serverAction: true
});

Returns:

PropertyTypeDescription
outputPathstringAbsolute path to the generated .tsx file
codestringGenerated TypeScript source
wroteFilebooleanWhether the file was written (false in dry-run or if exists without --force)
actionPathstring | undefinedPath to server action file (when serverAction: true)
actionCodestring | undefinedServer action source

createProgram()

Returns a Commander.js Command instance for embedding in custom CLIs:

import { createProgram } from '@zod-to-form/cli';

const program = createProgram();
await program.parseAsync(['node', 'zodform', 'generate', '--schema', /* ... */]);

defineComponentConfig(config)

Type-safe helper for component config files — see Component Config.

validateComponentConfig(value, source?)

Runtime validation for externally loaded config objects:

import { validateComponentConfig } from '@zod-to-form/cli';

const parsed = validateComponentConfig(loadedConfig, 'my-config.json');

Throws with descriptive error messages if the config shape is invalid.

CI Integration

Add to a build script in package.json:

{
"scripts": {
"generate:forms": "zodform generate --schema src/schemas/user.ts --export userSchema --out src/components/ --force"
}
}

Or add as a pre-build step in CI pipelines to ensure generated forms stay in sync with schema changes.

Relationship to Runtime

The CLI codegen and runtime <ZodForm> share @zod-to-form/core — the same walker produces the same FormField[] tree. A component config file can drive both paths. See Runtime Rendering.