Quickstart
Define a form, mount the handler, and render a fill session in Next.js App Router.
A fill session in Next.js App Router — drafts, showWhen, and snapshot validation.
Install
bash pnpm add @dimah-form/server @dimah-form/react
Define a form
import { defineForm } from "@dimah-form/server";
export const feedbackForm = defineForm({
title: "Product Feedback",
slug: "feedback",
fields: [
{ id: "fullName", type: "text", label: "Full name", required: true },
{
id: "rating",
type: "select",
label: "Rating",
required: true,
options: [
{ value: "great", label: "Great" },
{ value: "ok", label: "Okay" },
{ value: "poor", label: "Poor" },
],
},
{
id: "improvements",
type: "text",
label: "What could we improve?",
required: true,
showWhen: { field: "rating", equals: "poor" },
},
],
});import { feedbackForm } from "./feedback";
export const forms = {
feedback: feedbackForm,
};Create the instance
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { forms } from "@/lib/forms";
export const form = dimahForm({
database: memoryAdapter(),
forms,
basePath: "/api/form",
});
export type Form = typeof form;memoryAdapter() is process-local. For SQL, see Database.
Mount the handler
import { toNextJsHandler } from "@dimah-form/server/next";
import { form } from "@/lib/form";
export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(form);Create the client
"use client";
import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";
export const formClient = createFormClient<Form>({
basePath: "/api/form",
});
export const { useFormResponse } = formClient;Build the fill UI
"use client";
import { emptyToNull, fieldLabel, fieldOptions } from "@dimah-form/react";
import type { FormSnapshot } from "@dimah-form/react";
import { useFormResponse } from "@/lib/form-client";
import type { Form } from "@/lib/form";
export function Questionnaire({ form }: { form: FormSnapshot }) {
const q = useFormResponse<Form["$Infer"]["answers"]["feedback"]>({
snapshot: form,
autosave: { debounceMs: 1000 },
});
if (q.inactive) return <p>This form is inactive.</p>;
if (q.status === "submitted") return <p>Submitted.</p>;
return (
<form
onSubmit={(e) => {
e.preventDefault();
void q.submit();
}}
className="mx-auto max-w-md space-y-4"
>
<h1 className="text-xl font-bold">{form.title}</h1>
{q.visibleFields.map((field) => {
const binding = q.field(field.id);
const value = typeof binding.value === "string" ? binding.value : "";
return (
<div key={field.id} className="space-y-1">
<label className="block text-sm font-medium">
{fieldLabel(field)}
{binding.required && <span className="ml-1 text-red-500">*</span>}
</label>
{field.type === "select" ? (
<select
value={value}
disabled={binding.disabled}
onChange={(e) => binding.onChange(emptyToNull(e.target.value))}
className="w-full rounded border p-2"
>
<option value="">Select...</option>
{fieldOptions(field).map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
) : (
<input
value={value}
disabled={binding.disabled}
onChange={(e) => binding.onChange(emptyToNull(e.target.value))}
className="w-full rounded border p-2"
/>
)}
{binding.error && (
<p className="text-xs text-red-500">{binding.error}</p>
)}
</div>
);
})}
{q.error && <p className="text-sm text-red-500">{q.error}</p>}
<div className="flex gap-2">
<button
type="button"
disabled={q.pending !== undefined || q.locked}
onClick={() => void q.saveDraft()}
className="rounded border px-4 py-2"
>
{q.pending === "save" ? "Saving..." : "Save draft"}
</button>
<button
type="submit"
disabled={q.pending !== undefined || q.locked}
className="rounded bg-black px-4 py-2 text-white"
>
{q.pending === "submit" ? "Submitting..." : "Submit"}
</button>
</div>
</form>
);
}Render on a page
import { Questionnaire } from "@/components/questionnaire";
import { form } from "@/lib/form";
import { notFound } from "next/navigation";
export default async function Page() {
const snapshot = await form.api.getForm({
query: { formId: "feedback" },
});
if (!snapshot) notFound();
return <Questionnaire form={snapshot} />;
}