Server-validated forms
The @netscript/fresh form surface lets a Fresh page own a form end to end: parse the
posted payload on the server, validate it against a schema, surface field-level and
form-level errors, and render the same managed Form component on both the initial GET
and the failed POST. Reach for it when a page mutates data and must round-trip submitted
values and errors without a client framework, while keeping CSRF protection and idempotent
submissions in place.
This entrypoint is intentionally narrow. It exposes the shipped helper surface used by playground consumers and keeps deeper form internals out of the public package contract.
How the surface fits together
A server-validated form moves through three layers:
- Parse the incoming
FormDatainto a plain nested object withformDataToRawValues, then normalize empty strings toundefinedwithnormalizeFormValues. - Validate the normalized values. A schema adapter such as the one returned by
createStandardSchemaAdapterturns any Standard Schema v1 validator into aFormSchemaAdapter, whosesafeParsereturns a normalized success or failure result. On failure, errors flatten into the canonicalFormErrorsshape. - Render the managed
Formcomponent with the resolvedFormState. On a GET request the state starts from your initial values; on a failed POST it preserves the submitted values and errors so the page re-renders with the user's input intact.
resolveFormState is the bridge between the route handler and the component: it inspects
the handler data and either preserves an existing FormState or builds a fresh one from
initial values.
Defining and rendering a form
The Form component renders a managed <form> element with the submission and CSRF hidden
inputs already wired. It accepts a state (the resolved FormState-compatible value),
the form children, and optional formProps overrides.
import { Form, resolveFormState } from "@netscript/fresh/form";
interface ContactValues {
email: string;
message: string;
}
export default function ContactForm({ data }: { data: unknown }) {
const state = resolveFormState<ContactValues>(data, {
email: "",
message: "",
});
return (
<Form<ContactValues>
state={state}
formProps={ { action: "/contact", method: "POST" } }
>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" defaultValue={state.values.email} />
<label htmlFor="message">Message</label>
<textarea id="message" name="message">{state.values.message}</textarea>
<button type="submit">Send</button>
</Form>
);
}
FormState exposes exactly two members: values (the current Partial<TValues>) and
errors (a FormErrors<TValues> map). To read the first message for a field, call
firstFieldError(state.errors, "email").
Validating on the server
On the server, parse and validate the payload, then return a FormState from the handler
so the page can re-render with errors. Build a FormSchemaAdapter from any Standard Schema
v1 schema with createStandardSchemaAdapter, and convert a thrown validation error into
the canonical FormErrors shape with toFormErrors.
import {
createEmptyFormErrors,
createStandardSchemaAdapter,
formDataToRawValues,
normalizeFormValues,
toFormErrors,
} from "@netscript/fresh/form";
import type { FormState } from "@netscript/fresh/form";
interface ContactValues {
email: string;
message: string;
}
// `schema` is any Standard Schema v1 compatible validator.
const adapter = createStandardSchemaAdapter<typeof schema, ContactValues>(schema);
export async function handleContact(request: Request): Promise<FormState<ContactValues>> {
const formData = await request.formData();
const raw = formDataToRawValues(formData);
const values = normalizeFormValues<ContactValues>(raw);
const result = await adapter.safeParse(values);
if (!result.success) {
return {
values,
errors: toFormErrors<ContactValues>({
flatten: () => ({
fieldErrors: result.fieldErrors,
formErrors: result.formErrors,
}),
}),
};
}
// Run the mutation with the validated `result.data`, then redirect or return success.
return { values, errors: createEmptyFormErrors<ContactValues>() };
}
formDataToRawValues understands dotted paths and bracket indices (items[0].productId
becomes { items: [{ productId: "value" }] }), so nested values arrive in the shape your
schema expects. normalizeFormValues recursively converts empty strings to undefined
before validation runs.
CSRF protection
The form surface carries a CSRF token between the GET that renders the form and the POST
that submits it. Generate a token with generateCsrfToken, persist it with setCsrfCookie
on the response headers, and on submission read the cookie token with readCsrfToken and
compare it to the submitted token with verifyCsrfToken.
import {
generateCsrfToken,
readCsrfToken,
setCsrfCookie,
verifyCsrfToken,
CSRF_FIELD_NAME,
} from "@netscript/fresh/form";
// GET: issue a token and set the cookie.
const token = generateCsrfToken();
const headers = new Headers();
setCsrfCookie(headers, token, new URL(request.url));
// POST: verify the submitted token against the cookie.
const formData = await request.formData();
const submitted = formData.get(CSRF_FIELD_NAME)?.toString();
const cookieToken = readCsrfToken(request);
if (!verifyCsrfToken(cookieToken, submitted)) {
// Reject the submission.
}
The cookie name is fixed as CSRF_COOKIE_NAME ("ns_form_csrf") and the hidden field name
as CSRF_FIELD_NAME ("__csrf__"). The managed Form component renders that hidden CSRF
input for you when the state carries a token.
Idempotent submissions
Each rendered form can round-trip a stable submission identifier so a retried POST is not
processed twice. generateSubmissionId creates the identifier, and
getSubmissionHiddenInputProps returns the hidden input props that carry it under
SUBMISSION_ID_FIELD_NAME ("__submission_id__").
import {
generateSubmissionId,
getSubmissionHiddenInputProps,
} from "@netscript/fresh/form";
const submissionId = generateSubmissionId();
const hiddenProps = getSubmissionHiddenInputProps(submissionId);
Partial form regions
FormRegion renders a Fresh partial boundary so a form-driven update can replace, prepend,
or append a region of the page without a full navigation. It accepts the partial name,
an optional mode ("replace", "prepend", or "append"), and children.
import { FormRegion } from "@netscript/fresh/form";
<FormRegion name="contact-result" mode="replace">
{/* Region content updated by the form submission. */}
</FormRegion>;
API summary
Components
| Symbol | Description |
|---|---|
Form |
Render a managed form element with submission and CSRF hidden inputs. |
FormRegion |
Render a Fresh partial boundary for form-driven updates. |
FormProps<TValues> |
Props accepted by the managed form component (state, children, formProps, enhancement). |
FormRegionProps |
Props for the partial-region helper (name, mode, children). |
Parsing and state
| Symbol | Description |
|---|---|
formDataToRawValues |
Parse a FormData instance into a nested object, handling dotted paths and bracket indices. |
normalizeFormValues |
Normalize raw values by converting empty strings to undefined. |
resolveFormState |
Resolve form state from route handler data, preserving submitted values and errors when present. |
FormState<TValues> |
Lightweight shipped form state: values and errors. |
Validation
| Symbol | Description |
|---|---|
createStandardSchemaAdapter |
Create a FormSchemaAdapter from any Standard Schema v1 compatible schema. |
FormSchemaAdapter<TValues, TOutput> |
Validation boundary with parse, safeParse, getConstraints, and getDefaults. |
toFormErrors |
Convert a Zod-like validation error into the canonical FormErrors<T> shape. |
createEmptyFormErrors |
Create an empty form error map. |
firstFieldError |
Return the first error message for a field, if present. |
FormErrors<TValues> |
Field-level error map; the _form key stores form-wide errors. |
CSRF and idempotency
| Symbol | Description |
|---|---|
generateCsrfToken |
Generate a new CSRF token for a rendered form. |
readCsrfToken |
Read the current CSRF token from a request cookie header. |
setCsrfCookie |
Set the CSRF cookie on response headers. |
verifyCsrfToken |
Verify that the submitted token matches the cookie token. |
CSRF_COOKIE_NAME / CSRF_FIELD_NAME |
Cookie and hidden-field names used for the token. |
generateSubmissionId |
Create a new form submission identifier. |
getSubmissionHiddenInputProps |
Return the hidden input props that carry an idempotent submission id. |
SUBMISSION_ID_FIELD_NAME |
Hidden field name used to round-trip the submission id. |
Related
How a Fresh page is composed.
Pages and buildersThe define-page builder.
Routing and route contractsMap requests to handlers.
Data loading and the query cacheLoad and cache server data.
Deferred and streaming UIStream UI as data resolves.
Interactive islandsHydrate interactive regions.
- Pillar hub: Web Layer
- Flagship tutorial: Live dashboard
- Error handling and diagnostics · Testing Fresh pages · Examples and sandbox