@fillament/server
v1.0.0
Published
Reuse a Fillament validation schema on the server. Parse FormData into typed values, validate with the same zod/yup/JSON-Schema adapter your browser uses, and return the same machine-readable error shape — built for React 19 / Next server actions.
Maintainers
Readme
@fillament/server
Reuse a Fillament validation schema on the server. Parse FormData into typed values, validate with the same zod / yup / JSON-Schema adapter your browser uses, and return the same machine-readable error shape — built for React 19 / Next server actions.
pnpm add @fillament/serverFramework-agnostic and dependency-light: no React, no Next. It's the full-stack half of Fillament — one schema, one error vocabulary, on the client and the server.
Server action in three lines
// app/actions.ts
"use server";
import { zodAdapter } from "@fillament/zod";
import { createFormAction, emptyFormActionState } from "@fillament/server";
import { SignupSchema } from "./schema"; // the SAME schema the form uses
export const signupAction = createFormAction(zodAdapter(SignupSchema), async (values) => {
const user = await db.users.create(values); // only runs when valid
return { id: user.id };
});// signup-form.tsx
"use client";
import { useActionState } from "react";
import { emptyFormActionState } from "@fillament/server";
import { signupAction } from "./actions";
export function SignupForm() {
const [state, action] = useActionState(signupAction, emptyFormActionState());
return (
<form action={action}>
<input name="email" />
{state.fieldErrors.email && <p>{state.fieldErrors.email}</p>}
<input name="address.city" />
{state.fieldErrors["address.city"] && <p>{state.fieldErrors["address.city"]}</p>}
<button>Sign up</button>
</form>
);
}The server re-validates with your real schema even if the client is bypassed, and the errors it returns are addressed by the same field paths ("address.city") the form uses.
API
validateFormData(adapter, formData, options?)
Parse + validate. Returns a FormActionState:
const state = await validateFormData(zodAdapter(Schema), formData);
// {
// ok: boolean,
// values: Partial<TValues>, // parsed + coerced
// problem: ValidationProblem, // @fillament/agent's shape: { valid, errors:[{path,code,message}], formErrors }
// fieldErrors: Record<string,string>, // path → first message, for rendering
// }problem is byte-for-byte the shape @fillament/agent's toValidationProblem produces in the browser — so an agent (or your client) corrects against it the same way on either side.
createFormAction(adapter, handler, options?)
Wraps validateFormData into a useActionState-compatible action: it runs handler(values) only when valid and returns its result on state.data. On failure it returns the structured errors without calling the handler — the server-side mirror of the client's submit gate.
parseFormData(formData, options?)
FormData → nested values object. Dot-path names build objects and arrays (address.city, contacts.0.email); a repeated key (multi-select, checkbox group) becomes an array. With a schema (adapter or JSON Schema), string values are coerced to their declared types:
| Schema type | Coercion |
| --- | --- |
| number / integer | "42" → 42; empty string → omitted; non-numeric kept as-is so validation reports it |
| boolean | "on" / "true" / "1" → true, else false |
| everything else | kept as string (Files pass through untouched) |
Pass coerce: false to nest raw strings.
emptyFormActionState() / fieldErrorsFromProblem(problem)
A neutral initial state for useActionState, and a helper to flatten any ValidationProblem to path → first message.
Notes
- Required arrays need a default. An empty multi-select submits nothing, so the field is absent from
FormData. Give array fields.default([])(Zod) or make them optional, exactly as you would for anyFormData-backed form. - Files (file inputs) are passed through untouched — validate or store them yourself.
- Works in any server runtime with a
FormData(Next server actions, React 19 actions, Remix, plain Node handlers).
License
MIT © headlessButSmart
