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.
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" } });
}A runnable project for this example, opened in the bext play editor — edit the code and the preview updates.
Live-editable — the preview recompiles as you type.