Skip to content

Building a Typed Form Flow

This guide combines @vielzeug/validit, @vielzeug/formit, and @vielzeug/fetchit to build a typed form flow from user input to API submission. A validation schema is one reusable rule set for parsing and checking incoming data.

Problem

You need one schema definition that powers UI validation, submit payload typing, and server request handling.

Architecture

StepPackageResponsibility
Schema@vielzeug/validitDefine validation schema and parse unknown input
Form state@vielzeug/formitManage field values, touched state, and submit flow
Transport@vielzeug/fetchitSend validated payloads to an API endpoint

Runnable Example

ts
import { createApi } from '@vielzeug/fetchit';
import { createForm } from '@vielzeug/formit';
import { v, type Infer } from '@vielzeug/validit';

const UserSchema = v.object({
  email: v.string().trim().email(),
  name: v.string().min(2),
  age: v.coerce.number().int().min(18),
});

type UserInput = Infer<typeof UserSchema>;

const api = createApi({ baseUrl: '/api' });

const form = createForm<UserInput>({
  defaultValues: { email: '', name: '', age: 18 },
  validators: {
    email: (value) => {
      const result = v.string().trim().email().safeParse(value);
      return result.success ? undefined : result.error.message;
    },
    name: (value) => {
      const result = v.string().min(2).safeParse(value);
      return result.success ? undefined : result.error.message;
    },
    age: (value) => {
      const result = v.coerce.number().int().min(18).safeParse(value);
      return result.success ? undefined : result.error.message;
    },
  },
});

export async function submitRegistration(rawInput: unknown): Promise<void> {
  const parsed = UserSchema.safeParse(rawInput);
  if (!parsed.success) {
    const { fieldErrors } = parsed.error.flatten();
    console.error('Validation failed', fieldErrors);
    return;
  }

  await form.submit(async () => {
    await api.post('/users', { body: parsed.data });
  });
}

Expected Output

  • Invalid payloads fail fast with field-level errors.
  • Valid payloads are submitted with a typed request body.
  • Form submission status stays in sync with async request state.

Common Pitfalls

  • Defining one schema for UI and a different schema for submit payloads.
  • Casting unknown values instead of parsing them with safeParse.
  • Sending raw form strings without coercion for numeric fields.

See Also