Form validation

A single v.object({…}) describes the schema; the server validates and returns every per-field error in one pass plus the submitted values. The page reads both via actionData, renders inputs with error styling, and keeps the user's typing. A valid submit 303s to a confirmation URL — clear failure/success split.

Tip
Validation comes from @bext-stack/framework/validation — the same typed, dependency-free primitive any PRISM site uses. Every field reports all its errors in one pass; errorsByField returns the first per field (rule order gives you 'required THEN format' for free) and result.data is typed & coerced (age is a number).

Try it

This form uses the framework's v.object({…}) primitive — no more hand-rolled validate().

Try submitting empty, a bad email, or a username that's too short.

src/app/examples/validation/page.tsxTSX
import { v, validate, errorsByField } from "@bext-stack/framework/validation";

// One typed schema — every failing field reports in a single pass.
const Signup = v.object({
  email:    v.string().trim().nonempty().email(),
  username: v.string().trim().min(3).regex(/^[a-z0-9_]+$/i),
  age:      v.number().int().min(13).max(120),
});

export async function action({ request }) {
  const result = await validate(request, Signup);  // reads FormData | JSON
  if (!result.ok) {
    // re-render with per-field errors + the user's typed values
    return { ok: false, errors: errorsByField(result.errors) };
  }
  const user = result.data;   // { email, username, age: number } — typed & coerced
  await createUser(user);
  return new Response(null, { status: 303, headers: { Location: "/welcome" } });
}
Try it liveOpen in play

A runnable project for this example, opened in the bext play editor — edit the code and the preview updates.

src/app/page.tsx1 file · runs in your browser
Live-editable — the preview recompiles as you type.