# dimah-form

> Backend-first questionnaire engine: the library owns the protocol, definition snapshots, and submit validation. You own UI, auth, and the database adapter. Not a form renderer, and not a hosted survey product.

TypeScript packages: `@dimah-form/server` (`dimahForm()` handler and `api`), `@dimah-form/react` (thin client and `useFormResponse`). Optional `@dimah-form/db` is the FumaDB SQL adapter. Protocol types live in `@dimah-form/core`. There is no UI package.

HTTP adapters: Next.js App Router, Express, Hono, Fastify, Elysia, SvelteKit, and Node. Persistence is required: `memoryAdapter()` from `@dimah-form/server` (Quickstart), or optional `db()` from `@dimah-form/db` for SQL. Built-in field types: text, number, boolean, select, multiSelect, email, date. Extra types are `defineFieldType` validators, not components.

Use it when the app needs typed questionnaires with drafts, snapshots, and submit validation, while keeping widgets in the consumer app. Skip it when you want a ready-made form renderer, visual form builder, or hosted survey SaaS.

Install: `pnpm add @dimah-form/server @dimah-form/react`. Add `@dimah-form/db` only for production SQL.

- Auth stays in consumer `guard` hooks. Do not look for library auth.
- Persistence is `database`, not a plugin. Plugins add endpoints, hooks, field types, and error codes.
- Apps import from the package they already use: `@dimah-form/server` on the server, `@dimah-form/react` in the browser. Share `$Infer` with `export type Form = typeof form` and `createFormClient<Form>()`.
- Filling a response is headless: `useFormResponse` / `createFormResponseSession`. Consumers own widgets.
- Each response stores the definition it was started with. Submit validates that snapshot.


# Introduction (https://form.dimah.dev/docs)



**dimah-form** is a backend-first questionnaire engine. It freezes the form definition onto every response, validates answers against that snapshot, and gives React a thin fill-session hook. It does not render fields, ship widgets, or own your auth.

<Flow
  label="System Data Flow"
  steps="[
  {
    name: &#x22;React Client&#x22;,
    kind: &#x22;client&#x22;,
    note: &#x22;useFormResponse + your widgets&#x22;,
  },
  {
    name: &#x22;HTTP Handler&#x22;,
    kind: &#x22;server&#x22;,
    note: &#x22;dimahForm() + your guard&#x22;,
  },
  { name: &#x22;Persistence&#x22;, kind: &#x22;data&#x22;, note: &#x22;Snapshots and responses&#x22; },
]"
/>

***

## When to use it [#when-to-use-it]

Use it when you need typed questionnaires with drafts and server-side validation, and you already have a design system.

Skip it if you want a form renderer, a visual builder, or hosted survey SaaS.

The problems it is built around:

1. **Schema drift** — an admin can change a live form while someone is still filling a draft. dimah-form validates against the snapshot taken at start, not the live definition.
2. **Drafts** — respondents leave, switch devices, or drop packets. Drafts save incrementally, with `updatedAt` compare-and-swap so two tabs do not silently overwrite each other.
3. **Headless validation** — you render with Tailwind, shadcn/ui, or plain HTML. The server still checks required visible fields against the frozen snapshot.

***

## Packages [#packages]

```
@dimah-form/core
        ↓
@dimah-form/server  ←  @dimah-form/db (optional)
        │
@dimah-form/react
```

| Package                  | Where                    | What you import it for                                                             |
| :----------------------- | :----------------------- | :--------------------------------------------------------------------------------- |
| **`@dimah-form/core`**   | Shared                   | Protocol types, field validators, error codes. Server and React both depend on it. |
| **`@dimah-form/server`** | Node / Edge              | `dimahForm()`, HTTP adapters, `form.api`, `memoryAdapter()`.                       |
| **`@dimah-form/react`**  | Browser (`"use client"`) | `createFormClient()`, `useFormResponse()`, field helpers.                          |
| **`@dimah-form/db`**     | Server                   | Optional. Production SQL adapter via FumaDB.                                       |

***

## How to read these docs [#how-to-read-these-docs]

Start with the **Quickstart** if you want a working Next.js form. Read **Architecture** and **Snapshots** when you need the model, not just the copy-paste.

<Cards>
  <Card title="Quickstart" href="/docs/quickstart" description="Install, define a form, mount the handler, and render a fill UI." />

  <Card title="Architecture" href="/docs/architecture" description="Package boundaries, data flow, and the request pipeline." />

  <Card title="Forms" href="/docs/forms" description="defineForm, field rules, showWhen, and $Infer." />

  <Card title="React Client" href="/docs/react" description="useFormResponse: visibility, drafts, and submit." />
</Cards>

After that, pick the layer you are implementing:

* **Client** — [React](https://form.dimah.dev/docs/react) and [Widgets](https://form.dimah.dev/docs/widgets)
* **Backend** — [Server](https://form.dimah.dev/docs/server), [Database](https://form.dimah.dev/docs/database), [Auth](https://form.dimah.dev/docs/auth)
* **Extending** — [Custom Fields](https://form.dimah.dev/docs/custom-fields) and [Plugins](https://form.dimah.dev/docs/plugins)
* **Reference** — [Configuration](https://form.dimah.dev/docs/configuration), [Field Types](https://form.dimah.dev/docs/field-types), [Protocol](https://form.dimah.dev/docs/protocol), [Errors](https://form.dimah.dev/docs/errors)


# Quickstart (https://form.dimah.dev/docs/quickstart)



A fill session in Next.js App Router — drafts, `showWhen`, and snapshot validation.

<Steps>
  <Step>
    ### Install [#install]

    <Tabs items="[&#x22;pnpm&#x22;, &#x22;npm&#x22;, &#x22;yarn&#x22;, &#x22;bun&#x22;]">
      <Tab value="pnpm">
        `bash pnpm add @dimah-form/server @dimah-form/react `
      </Tab>

      <Tab value="npm">
        `bash npm install @dimah-form/server @dimah-form/react `
      </Tab>

      <Tab value="yarn">
        `bash yarn add @dimah-form/server @dimah-form/react `
      </Tab>

      <Tab value="bun">
        `bash bun add @dimah-form/server @dimah-form/react `
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### Define a form [#define-a-form]

    ```ts title="lib/forms/feedback.ts"
    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" },
        },
      ],
    });
    ```

    ```ts title="lib/forms/index.ts"
    import { feedbackForm } from "./feedback";

    export const forms = {
      feedback: feedbackForm,
    };
    ```
  </Step>

  <Step>
    ### Create the instance [#create-the-instance]

    ```ts title="lib/form.ts"
    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](https://form.dimah.dev/docs/database).
  </Step>

  <Step>
    ### Mount the handler [#mount-the-handler]

    ```ts title="app/api/form/[...all]/route.ts"
    import { toNextJsHandler } from "@dimah-form/server/next";
    import { form } from "@/lib/form";

    export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(form);
    ```
  </Step>

  <Step>
    ### Create the client [#create-the-client]

    ```ts title="lib/form-client.ts"
    "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;
    ```
  </Step>

  <Step>
    ### Build the fill UI [#build-the-fill-ui]

    ```tsx title="components/questionnaire.tsx"
    "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>
      );
    }
    ```
  </Step>

  <Step>
    ### Render on a page [#render-on-a-page]

    ```tsx title="app/page.tsx"
    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} />;
    }
    ```
  </Step>
</Steps>

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Architecture" href="/docs/architecture" description="Package boundaries, snapshots, and the request pipeline." />

  <Card title="Forms" href="/docs/forms" description="defineForm, field rules, and showWhen." />

  <Card title="React Client" href="/docs/react" description="useFormResponse: visibility, drafts, and submit." />

  <Card title="Database" href="/docs/database" description="Keep memoryAdapter, or add SQL with FumaDB." />
</Cards>


# Architecture (https://form.dimah.dev/docs/architecture)



The library owns the protocol, definition snapshots, and submit validation. Your app owns widgets, auth, and the database adapter. Those four boundaries are the architecture.

***

## Principles [#principles]

<Cards>
  <Card title="Headless by Design" description="No widgets, CSS, or component registry. Render fields with the design system you already have." />

  <Card title="Snapshot Invariant" description="A fill freezes the definition at start. Editing the live form does not break in-flight drafts." />

  <Card title="Protocol Single Source of Truth" description="@dimah-form/core defines all routes, payloads, and validators. Server and client never drift." />

  <Card title="Zero-ORM Server Engine" description="@dimah-form/server has no database dependencies. Storage is abstracted through the ResponseStore interface." />
</Cards>

***

## System Flow [#system-flow]

The diagram shows how widgets, HTTP routes, `guard`, validation, and the store fit together:

<ArchitectureDiagram />

<Accordions>
  <Accordion title="View Mermaid Diagram">
    ```mermaid
    flowchart TD
      subgraph Client ["Browser / React App"]
        UI["Your UI Widgets / Stepper"] --> Hook["useFormResponse"]
        Hook --> HTTPClient["createFormClient"]
      end

      subgraph Server ["Backend Runtime (@dimah-form/server)"]
        HTTPClient --> Route["HTTP Handler (Next.js / Hono / Express)"]
        Route --> Guard["guard ({ request, operation, ... })"]
        Guard --> Engine["dimahForm() Router"]
        Engine --> SnapshotVal["Validate vs response.definition Snapshot"]
        SnapshotVal --> PreHooks["on* Pre-Write Lifecycle Hooks"]
      end

      subgraph Storage ["Persistence (ResponseStore)"]
        PreHooks --> Store["memoryAdapter, db, or custom"]
        Store --> Tables[("questionnaire & response rows")]
        Store --> PostHooks["after* Post-Write Lifecycle Hooks"]
      end

      LiveDoc["Code Forms / DB Form"] -.->|"freezes snapshot at start"| Tables
    ```
  </Accordion>
</Accordions>

***

## Package Boundaries [#package-boundaries]

Each package has one job and a one-way dependency graph:

| Package                  | Environment | Responsibility                                                                                         | Key Exports                                                             |
| :----------------------- | :---------- | :----------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- |
| **`@dimah-form/core`**   | Universal   | Protocol SSOT, Zod schemas, field definitions, answer validation, error codes, and type inference.     | `defineForm`, `defineFieldType`, `APIError`, `FORM_ERROR_CODES`         |
| **`@dimah-form/server`** | Server      | Backend router, HTTP framework adapters, `form.api`, lifecycle hook dispatcher, and `memoryAdapter()`. | `dimahForm()`, `toNextJsHandler`, `toHonoHandler`, `memoryAdapter`      |
| **`@dimah-form/react`**  | Browser     | Headless React client, session state machine, visibility resolver, field bindings, and autosave.       | `createFormClient()`, `useFormResponse()`, `fieldLabel`, `fieldOptions` |
| **`@dimah-form/db`**     | Server      | Optional. Production SQL adapter using FumaDB for Drizzle, Prisma, and Kysely.                         | `DimahFormDB`, `db()`                                                   |

***

## Request Pipeline [#request-pipeline]

Every mutation and query runs the same six stages:

<Flow
  label="Execution Lifecycle"
  steps="[
  { name: &#x22;1. Parse&#x22;, kind: &#x22;protocol&#x22;, note: &#x22;Zod query / body check&#x22; },
  { name: &#x22;2. Guard&#x22;, kind: &#x22;server&#x22;, note: &#x22;Auth & permissions&#x22; },
  { name: &#x22;3. Validate&#x22;, kind: &#x22;protocol&#x22;, note: &#x22;Against snapshot&#x22; },
  { name: &#x22;4. on* Hook&#x22;, kind: &#x22;server&#x22;, note: &#x22;Pre-write mutation&#x22; },
  { name: &#x22;5. Persist&#x22;, kind: &#x22;data&#x22;, note: &#x22;Store write & CAS&#x22; },
  { name: &#x22;6. after* Hook&#x22;, kind: &#x22;server&#x22;, note: &#x22;Side-effects&#x22; },
]"
/>

### 1. Inbound Parse & Schema Validation [#1-inbound-parse--schema-validation]

The HTTP adapter parses the incoming HTTP request and validates query parameters or body payloads against the Zod schemas defined in `@dimah-form/core`. Malformed requests immediately return `400 VALIDATION_ERROR`.

### 2. Security Guard [#2-security-guard]

The optional `guard` hook executes before any database or form logic. You inspect the `request`, `operation`, `formId`, or `responseId`, verify user session cookies/tokens, and enforce permissions. Throwing an `APIError` halts execution and sends a structured error response.

### 3. Snapshot Validation [#3-snapshot-validation]

When saving drafts or submitting responses, answer values are validated **against the frozen `response.definition` snapshot**, never against the live form schema. Submitting verifies that all visible required fields are populated and valid. Hidden fields (per `showWhen`) are automatically stripped.

### 4. Pre-Write Hooks (`on*`) [#4-pre-write-hooks-on]

Pre-persistence hooks (such as `onStart`, `onDraft`, `onSubmit`) run before the database transaction. You can attach user identifiers (`response.respondentId = user.id`) or enrich metadata directly on the in-memory record.

### 5. Persistence & Optimistic Concurrency (CAS) [#5-persistence--optimistic-concurrency-cas]

The `ResponseStore` writes the record to the database. If an `expectedUpdatedAt` timestamp was provided, the store verifies that the row has not been modified by another client in the meantime. If the timestamp differs, a `STALE_UPDATE` (409) conflict is raised.

### 6. Post-Write Hooks (`after*`) [#6-post-write-hooks-after]

After the database write successfully commits, post-persistence hooks (such as `afterSubmit`, `afterDraft`) trigger side-effects, such as sending confirmation emails, posting webhooks, or enqueuing background worker jobs.

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Snapshots" href="/docs/snapshots" description="Learn how snapshots ensure schema immutability and handle draft lifecycles." />

  <Card title="Forms" href="/docs/forms" description="Author schemas with defineForm, configure field types, and enable $Infer." />

  <Card title="React Client" href="/docs/react" description="Deep dive into useFormResponse, visible fields, and draft actions." />

  <Card title="Server" href="/docs/server" description="Mount HTTP adapters across Next.js, Hono, Express, Fastify, and SvelteKit." />
</Cards>


# Snapshots (https://form.dimah.dev/docs/snapshots)



One of the foundational invariants of **dimah-form** is **Snapshot Immutability**: when a fill session begins, the system takes an exact deep snapshot of the form schema and stores it directly inside `response.definition`.

<Callout type="info">
  **The Snapshot Invariant**: All draft patching, conditional visibility
  calculations (`showWhen&#x60;), and final submission validations execute against
  &#x2A;*`response.definition`**, never against the live form schema.
</Callout>

***

## Why Snapshots Matter [#why-snapshots-matter]

In real-world applications, form schemas are constantly evolving. An administrator might:

* Add a new required field.
* Remove an existing field.
* Modify option values in a select dropdown.
* Change conditional visibility rules.

Without snapshots, an in-progress draft started on Monday would fail validation on Wednesday because the live form schema changed mid-fill.

With dimah-form:

1. Active responses stay **completely isolated** from live schema edits.
2. In-flight drafts can always be completed and submitted cleanly.
3. Historical submitted responses preserve the exact questions and options that the respondent saw when they filled the form.

***

## Response Statuses & State Machine [#response-statuses--state-machine]

A response record transitions through three possible statuses:

```mermaid
stateDiagram-v2
    [*] --> draft: startResponse()
    draft --> draft: saveDraft()
    draft --> submitted: submitResponse()
    draft --> abandoned: abandonResponse()
    submitted --> draft: reopenResponse()
    abandoned --> draft: reopenResponse()
    submitted --> [*]: deleteResponse()
    abandoned --> [*]: deleteResponse()
```

| Status          | Meaning                                                           | Allowed API Actions                               |
| :-------------- | :---------------------------------------------------------------- | :------------------------------------------------ |
| **`draft`**     | In-progress fill session. Required fields can be empty.           | `saveDraft`, `submitResponse`, `abandonResponse`  |
| **`submitted`** | Finalized and locked. All visible required fields were validated. | `reopenResponse`, `deleteResponse`, `getResponse` |
| **`abandoned`** | Closed by user or admin without submitting. Locked from edits.    | `reopenResponse`, `deleteResponse`, `getResponse` |

***

## Lifecycle Methods [#lifecycle-methods]

<Flow
  label="Response Lifecycle Stages"
  steps="[
  { name: &#x22;1. Start&#x22;, kind: &#x22;protocol&#x22;, note: &#x22;Freeze definition snapshot&#x22; },
  {
    name: &#x22;2. Draft&#x22;,
    kind: &#x22;client&#x22;,
    note: &#x22;Partial answer patches (null deletes)&#x22;,
  },
  {
    name: &#x22;3. Submit&#x22;,
    kind: &#x22;server&#x22;,
    note: &#x22;Validate visible fields vs snapshot&#x22;,
  },
  { name: &#x22;4. Reopen&#x22;, kind: &#x22;data&#x22;, note: &#x22;Unlock row back to draft&#x22; },
]"
/>

### 1. `startResponse` [#1-startresponse]

* Resolves the target form by `id` or `slug`.
* Verifies that the form has `status: "active"`.
* Freezes the form definition into `response.definition`.
* Seeds any configured `defaultValue`s into `response.answers`.
* If called with `{ resume: true, respondentId: "user-123" }`, it checks for an existing unfinished draft and returns it instead of creating a duplicate.

### 2. `saveDraft` [#2-savedraft]

* Accepts partial answer patches.
* Passing `null` as an answer value deletes that key from `answers`.
* **Does not require** visible required fields to be filled, enabling incremental progress across multi-page forms.
* Verifies that the record status is currently `draft`.

### 3. `submitResponse` [#3-submitresponse]

* Accepts final answer updates.
* Evaluates all conditional `showWhen` visibility rules against the current answers.
* Strips any hidden fields from the persisted answers so stale answers do not pollute your database.
* Validates that every **visible** field satisfies its validation rules and `required` constraints.
* Sets `status: "submitted"` and records `submittedAt: new Date().toISOString()`.

### 4. `abandonResponse` [#4-abandonresponse]

* Sets `status: "abandoned"`.
* Locks the response row from further draft edits.

### 5. `reopenResponse` [#5-reopenresponse]

* Moves a `submitted` or `abandoned` response back to `draft`.
* Preserves the original definition snapshot and answer values, allowing the respondent to edit and re-submit.

***

## Optimistic Concurrency Control (CAS) [#optimistic-concurrency-control-cas]

When multiple tabs are open or when network connections reconnect, concurrent writes can accidentally overwrite newer answers.

dimah-form implements &#x2A;*Compare-And-Swap (CAS)** concurrency control using the `updatedAt` timestamp:

1. When the client loads or saves a draft, it receives the record's current `updatedAt` ISO timestamp.
2. Subsequent `saveDraft` or `submitResponse` requests include `updatedAt`.
3. The database adapter compares `expectedUpdatedAt` against the stored row:
   * If they match, the update commits and a new `updatedAt` timestamp is generated.
   * If they differ (because another tab or request wrote to the row first), the server rejects the request with a `409 STALE_UPDATE` error.
4. The `useFormResponse` hook automatically catches `STALE_UPDATE`, refreshes the latest server state, and notifies your UI.

***

## The Response Record Schema [#the-response-record-schema]

Every response record in your database conforms to the following TypeScript structure:

```ts
type ResponseRecord = {
  /** Unique response identifier (e.g., CUID or UUID) */
  id: string;

  /** Foreign key to the parent form */
  formId: string;

  /** Current lifecycle status */
  status: "draft" | "submitted" | "abandoned";

  /** Frozen snapshot of the form definition at start time */
  definition: FormSnapshot;

  /** Key-value dictionary of respondent answers */
  answers: Record<string, unknown>;

  /** Optional user or session identifier */
  respondentId: string | null;

  /** Timestamp when the response was finalized */
  submittedAt: string | null;

  /** Record creation timestamp */
  createdAt: string;

  /** Last update timestamp (used for CAS optimistic locking) */
  updatedAt: string;
};
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Forms" href="/docs/forms" description="Learn how to structure form schemas, fields, and conditional showWhen rules." />

  <Card title="React Client" href="/docs/react" description="Wire useFormResponse to your UI components with automatic draft autosaving." />

  <Card title="Database" href="/docs/database" description="Required adapter: memory, optional FumaDB, or a custom store." />
</Cards>


# Forms (https://form.dimah.dev/docs/forms)



Forms are TypeScript (`defineForm`) or rows in your database (`saveForm`). Code-authored forms feed `$Infer`. Database forms do not.

***

## Authoring Forms in TypeScript [#authoring-forms-in-typescript]

We recommend defining each questionnaire in a dedicated file under `lib/forms/`:

```ts title="lib/forms/onboarding.ts"
import { defineForm } from "@dimah-form/server";

export const onboardingForm = defineForm({
  title: "Employee Onboarding",
  slug: "onboarding",
  description: "Complete your profile and equipment preferences.",
  status: "active",
  fields: [
    {
      id: "fullName",
      type: "text",
      label: "Full Name",
      required: true,
      minLength: 2,
      maxLength: 100,
    },
    {
      id: "department",
      type: "select",
      label: "Department",
      required: true,
      options: [
        { value: "engineering", label: "Engineering" },
        { value: "design", label: "Product Design" },
        { value: "marketing", label: "Marketing" },
      ],
    },
    {
      id: "githubHandle",
      type: "text",
      label: "GitHub Username",
      required: true,
      showWhen: {
        field: "department",
        equals: "engineering",
      },
    },
    {
      id: "portfolioUrl",
      type: "text",
      label: "Portfolio Link",
      showWhen: {
        field: "department",
        equals: "design",
      },
    },
    {
      id: "newsletter",
      type: "boolean",
      label: "Subscribe to company updates",
      defaultValue: true,
    },
  ],
});
```

Export your forms from a central index:

```ts title="lib/forms/index.ts"
import { onboardingForm } from "./onboarding";

export const forms = {
  onboarding: onboardingForm,
};
```

Pass the forms dictionary into `dimahForm()`:

```ts title="lib/form.ts"
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(),
  forms,
});

export type Form = typeof form;
```

***

## Form Properties [#form-properties]

| Property      | Type                                | Default     | Description                                                                    |
| :------------ | :---------------------------------- | :---------- | :----------------------------------------------------------------------------- |
| `title`       | `string`                            | —           | Human-readable title of the questionnaire.                                     |
| `slug`        | `string`                            | Catalog key | Unique URL-friendly slug. `getForm` and `startResponse` accept `id` or `slug`. |
| `description` | `string`                            | `undefined` | Optional subtitle or instructions for respondents.                             |
| `status`      | `"active" \| "draft" \| "archived"` | `"active"`  | Only `"active"` forms can start new response sessions.                         |
| `fields`      | `FormField[]`                       | `[]`        | Array of field definitions.                                                    |
| `meta`        | `Record<string, unknown>`           | `{}`        | Custom JSON dictionary for UI layout hints, icons, or category tags.           |

***

## Form Status Lifecycle [#form-status-lifecycle]

* **`active`**: Form is published and accepts new responses via `startResponse`.
* **`draft`**: Form is still being designed. Starting a response returns `409 FORM_INACTIVE`.
* **`archived`**: Form is deprecated. Existing submitted responses remain readable, but new responses are blocked.

***

## Field Anatomy [#field-anatomy]

Each item in `fields` requires an `id`, a `type`, and an optional `label`:

```ts
{
  id: "workEmail",
  type: "email",
  label: "Work Email Address",
  required: true,
  defaultValue: "user@example.com",
  meta: {
    placeholder: "you@company.com",
    tooltip: "We will send your onboarding packet here.",
  },
}
```

### Common Field Attributes [#common-field-attributes]

* **`id`**: Unique string identifier within the form. Serves as the key in the answers dictionary.
* **`type`**: Built-in type (`"text"`, `"number"`, `"boolean"`, `"select"`, `"multiSelect"`, `"email"`, `"date"`) or a custom type registered via `defineFieldType`.
* **`label`**: Display label for UI components (retrieved via `fieldLabel(field)`).
* **`required`**: If `true`, the field must be non-empty when submitting the form (if visible).
* **`defaultValue`**: Seeded into the answers map upon calling `startResponse`.
* **`showWhen`**: Conditional visibility rules.
* **`meta`**: Arbitrary metadata for UI customization (e.g., placeholder, step number, layout width).

***

## Conditional Visibility (`showWhen`) [#conditional-visibility-showwhen]

Fields can dynamically show or hide based on the current answers. When a field is hidden, it is excluded from `visibleFields` and its answer is stripped before final submission.

### 1. Simple Conditions [#1-simple-conditions]

```ts
// Check exact value match
showWhen: { field: "role", equals: "manager" }

// Check inequality
showWhen: { field: "plan", notEquals: "free" }

// Check if a multiSelect array contains a specific option
showWhen: { field: "techStack", includes: "typescript" }
```

### 2. Compound Conditions (`all` / `any`) [#2-compound-conditions-all--any]

```ts
// AND condition (all must match)
showWhen: {
  all: [
    { field: "experience", equals: "senior" },
    { field: "location", equals: "remote" },
  ],
}

// OR condition (at least one must match)
showWhen: {
  any: [
    { field: "department", equals: "engineering" },
    { field: "department", equals: "product" },
  ],
}
```

***

## Code-Authored vs Database Forms [#code-authored-vs-database-forms]

| Feature                  | Code-Authored Forms (`forms: {}`)                | Dynamic Database Forms (`saveForm()`)        |
| :----------------------- | :----------------------------------------------- | :------------------------------------------- |
| **Declaration**          | TypeScript files via `defineForm`                | Admin UI / CMS via `saveForm` API endpoint   |
| **Type Inference**       | Full compile-time `$Infer` typing                | `Record<string, unknown>`                    |
| **Version Control**      | Tracked in Git alongside application code        | Stored in the `questionnaire` database table |
| **Overwrite Protection** | Protected from accidental API deletion / updates | Can be modified or deleted via API routes    |

***

## End-to-End Type Inference (`$Infer`) [#end-to-end-type-inference-infer]

When you export `export type Form = typeof form`, dimah-form automatically derives the exact shape of your answers:

```ts
import type { Form } from "@/lib/form";

type OnboardingAnswers = Form["$Infer"]["answers"]["onboarding"];

// TypeScript automatically infers:
// type OnboardingAnswers = {
//   fullName: string;
//   department: "engineering" | "design" | "marketing";
//   githubHandle?: string;
//   portfolioUrl?: string;
//   newsletter?: boolean;
// }
```

Use this type in your React client:

```tsx
const q = useFormResponse<Form["$Infer"]["answers"]["onboarding"]>({
  snapshot: formSnapshot,
});
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="React Client" href="/docs/react" description="Learn how useFormResponse handles visible fields and answer bindings." />

  <Card title="Widgets" href="/docs/widgets" description="Build reusable UI widgets for text, select, and boolean fields." />

  <Card title="Field Types Reference" href="/docs/field-types" description="Explore built-in field validation constraints and options." />
</Cards>


# React Client (https://form.dimah.dev/docs/react)



`@dimah-form/react` is a headless fill-session runtime. It tracks visible fields, local answers, draft autosave, and submit state. It does not ship inputs or CSS.

***

## Setting Up the Client [#setting-up-the-client]

Create a shared client instance configured with your server's base path and type definition:

```ts title="lib/form-client.ts"
"use client";

import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
});

export const { useFormClient, useFormResponse } = formClient;
```

***

## The `useFormResponse` Hook [#the-useformresponse-hook]

The `useFormResponse` hook connects your UI to a questionnaire fill session:

```tsx title="components/questionnaire.tsx"
"use client";

import { emptyToNull, fieldLabel, fieldOptions } from "@dimah-form/react";
import type { FormSnapshot, ResponseRecord } from "@dimah-form/react";
import { useFormResponse } from "@/lib/form-client";
import type { Form } from "@/lib/form";

interface QuestionnaireProps {
  form: FormSnapshot;
  response?: ResponseRecord;
}

export function Questionnaire({ form, response }: QuestionnaireProps) {
  const q = useFormResponse<Form["$Infer"]["answers"]["onboarding"]>({
    snapshot: form,
    response, // Pass existing row when editing an existing draft
    autosave: { debounceMs: 1000 },
    validate: "submit", // "submit" (default) or "change"
    onSubmitted: (row) => {
      console.log("Successfully submitted:", row.id);
    },
  });

  if (q.inactive) {
    return <p>This form is currently unavailable.</p>;
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void q.submit();
      }}
      className="mx-auto max-w-md space-y-4"
    >
      <h2 className="text-xl font-bold">{form.title}</h2>

      {/* Render visible fields based on showWhen logic */}
      {q.visibleFields.map((field) => {
        const binding = q.field(field.id);

        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>
            <input
              type="text"
              value={typeof binding.value === "string" ? binding.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>
  );
}
```

***

## Session State Reference [#session-state-reference]

`useFormResponse` returns a comprehensive state object:

| Property              | Type                                                                    | Description                                                                         |
| :-------------------- | :---------------------------------------------------------------------- | :---------------------------------------------------------------------------------- |
| **`q.visibleFields`** | `FormField[]`                                                           | Fields that satisfy `showWhen` conditions based on current answers.                 |
| **`q.answers`**       | `TAnswers`                                                              | Current in-memory dictionary of answers.                                            |
| **`q.status`**        | `"draft" \| "submitted" \| "abandoned"`                                 | Lifecycle status of the current response.                                           |
| **`q.dirty`**         | `boolean`                                                               | `true` if local answers have unpersisted modifications.                             |
| **`q.pending`**       | `"save" \| "submit" \| "reopen" \| "abandon" \| "refresh" \| undefined` | Active asynchronous mutation in flight.                                             |
| **`q.locked`**        | `boolean`                                                               | `true` if `status !== "draft"` (response is frozen from edits).                     |
| **`q.error`**         | `string \| undefined`                                                   | Top-level error message from the last failed operation.                             |
| **`q.issues`**        | `Record<string, string>`                                                | Map of field ID to human-readable validation error message.                         |
| **`q.issueCodes`**    | `Record<string, string>`                                                | Map of field ID to machine-readable issue code (e.g., `"REQUIRED"`, `"TOO_SHORT"`). |
| **`q.completion`**    | `{ answered, total, percent }`                                          | Progress metrics for visible fields.                                                |
| **`q.inactive`**      | `boolean`                                                               | `true` if the form status is not `"active"` and no existing response was loaded.    |

***

## Actions & Methods [#actions--methods]

### `q.field(id)` [#qfieldid]

Returns a typed `FormFieldBinding` helper object for connecting an input control:

```ts
const {
  field, // The FormField definition object
  value, // Current answer value (or undefined)
  error, // Error message string for this field
  errorCode, // Issue code (e.g. "REQUIRED")
  disabled, // boolean (true when locked or pending)
  required, // boolean
  onChange, // (value: unknown) => void
  onBlur, // () => void
} = q.field("fullName");
```

### `q.setAnswer(id, value)` [#qsetanswerid-value]

Directly sets an answer value in the local state. Passing `null` removes the answer key.

### `q.saveDraft(options?)` [#qsavedraftoptions]

Explicitly saves the current answers to the backend as a draft with optimistic concurrency (`updatedAt`) checks.

### `q.submit(options?)` [#qsubmitoptions]

Validates all visible required fields against the snapshot schema. If valid, persists answers and locks the response to `"submitted"`.

### `q.reopen()` [#qreopen]

Unlocks a previously `"submitted"` or `"abandoned"` response back to `"draft"`.

### `q.abandon()` [#qabandon]

Marks the current draft as `"abandoned"`, preventing further edits.

### `q.refresh()` [#qrefresh]

Fetches the latest response record from the server and syncs local state.

### `q.reset()` [#qreset]

Resets in-memory answers back to the last persisted server state.

***

## Advanced Features [#advanced-features]

### 1. Automatic Draft Autosave [#1-automatic-draft-autosave]

Enable debounced background autosaving so users never lose progress:

```tsx
const q = useFormResponse({
  snapshot: form,
  autosave: { debounceMs: 1500 }, // Debounce timer in milliseconds
});
```

### 2. Validation Timing (`validate`) [#2-validation-timing-validate]

* **`"submit"` (default)**: Validates required fields only when the user attempts to submit. Once submitted, subsequent edits immediately clear resolved errors.
* **`"change"`**: Re-validates fields in real-time as the user types.

```tsx
const q = useFormResponse({
  snapshot: form,
  validate: "change",
});
```

### 3. Resuming Existing User Drafts [#3-resuming-existing-user-drafts]

If your users are authenticated, pass `resume: true` with a `respondentId` to automatically reconnect to their existing draft:

```tsx
const q = useFormResponse({
  snapshot: form,
  respondentId: session.user.id,
  resume: true,
});
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Widgets" href="/docs/widgets" description="Build a reusable widget dispatcher with Tailwind CSS and shadcn/ui." />

  <Card title="Auth" href="/docs/auth" description="Protect endpoints and associate respondentId with server sessions." />

  <Card title="Errors" href="/docs/errors" description="Handle validation issue codes and internationalize error messages." />
</Cards>


# Widgets (https://form.dimah.dev/docs/widgets)



dimah-form does not ship CSS or widgets. `q.field(id)` returns a `FormFieldBinding` you pass to your own `<input>`, shadcn/ui, Radix, or design-system control.

***

## Anatomy of `FormFieldBinding` [#anatomy-of-formfieldbinding]

When you call `q.field(field.id)`, `useFormResponse` returns a binding object tailored for input components:

```ts
type FormFieldBinding = {
  field?: FormField; // Field schema definition
  value: unknown; // Current value from answers map
  error?: string; // Active error message string
  errorCode?: string; // Machine-readable issue code (e.g., "REQUIRED")
  errorParams?: Record<string, string | number>; // Interpolation params
  disabled: boolean; // true if locked, pending, or disabled
  required: boolean; // true if required by schema
  onChange: (value: unknown) => void; // Value updater (pass null to clear)
  onBlur: () => void; // Blur handler for touch tracking
};
```

***

## Building a Unified Widget Dispatcher [#building-a-unified-widget-dispatcher]

A common and scalable pattern is to create a single `FormFieldControl` component that branches on `field.type`:

```tsx title="components/form-field-control.tsx"
"use client";

import {
  emptyToNull,
  fieldLabel,
  fieldOptions,
  type FormFieldBinding,
} from "@dimah-form/react";

export function FormFieldControl(binding: FormFieldBinding) {
  const { field, value, error, disabled, required, onChange } = binding;
  if (!field) return null;

  const label = (
    <label className="block text-sm font-semibold text-gray-800">
      {fieldLabel(field)}
      {required && <span className="ml-1 text-red-500">*</span>}
    </label>
  );

  // 1. Boolean (Checkbox)
  if (field.type === "boolean") {
    return (
      <div className="space-y-1">
        <label className="flex cursor-pointer items-center gap-2">
          <input
            type="checkbox"
            checked={value === true}
            disabled={disabled}
            onChange={(e) => onChange(e.target.checked ? true : null)}
            className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
          />
          <span className="text-sm text-gray-800">{fieldLabel(field)}</span>
        </label>
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // 2. Select (Dropdown)
  if (field.type === "select") {
    return (
      <div className="space-y-1">
        {label}
        <select
          value={typeof value === "string" ? value : ""}
          disabled={disabled}
          onChange={(e) => onChange(emptyToNull(e.target.value))}
          className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 focus:ring-2 focus:ring-black focus:outline-none"
        >
          <option value="">Select an option...</option>
          {fieldOptions(field).map((opt) => (
            <option key={opt.value} value={opt.value}>
              {opt.label}
            </option>
          ))}
        </select>
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // 3. Multi-Select (Checkbox Group)
  if (field.type === "multiSelect") {
    const selected = Array.isArray(value) ? (value as string[]) : [];

    const toggleOption = (optValue: string) => {
      const next = selected.includes(optValue)
        ? selected.filter((v) => v !== optValue)
        : [...selected, optValue];
      onChange(next.length > 0 ? next : null);
    };

    return (
      <div className="space-y-2">
        {label}
        <div className="grid grid-cols-2 gap-2">
          {fieldOptions(field).map((opt) => (
            <label
              key={opt.value}
              className="flex cursor-pointer items-center gap-2 text-sm text-gray-700"
            >
              <input
                type="checkbox"
                checked={selected.includes(opt.value)}
                disabled={disabled}
                onChange={() => toggleOption(opt.value)}
                className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
              />
              {opt.label}
            </label>
          ))}
        </div>
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // 4. Number Input
  if (field.type === "number") {
    return (
      <div className="space-y-1">
        {label}
        <input
          type="number"
          value={typeof value === "number" ? value : ""}
          disabled={disabled}
          onChange={(e) => {
            const raw = e.target.value;
            onChange(raw === "" ? null : Number(raw));
          }}
          className="w-full rounded-md border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-black focus:outline-none"
        />
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // 5. Date Input (YYYY-MM-DD)
  if (field.type === "date") {
    return (
      <div className="space-y-1">
        {label}
        <input
          type="date"
          value={typeof value === "string" ? value : ""}
          disabled={disabled}
          onChange={(e) => onChange(emptyToNull(e.target.value))}
          className="w-full rounded-md border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-black focus:outline-none"
        />
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // 6. Text & Email Input (Default)
  return (
    <div className="space-y-1">
      {label}
      <input
        type={field.type === "email" ? "email" : "text"}
        value={typeof value === "string" ? value : ""}
        disabled={disabled}
        onChange={(e) => onChange(emptyToNull(e.target.value))}
        className="w-full rounded-md border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-black focus:outline-none"
      />
      {error && <p className="text-xs text-red-500">{error}</p>}
    </div>
  );
}
```

***

## Utility Functions [#utility-functions]

`@dimah-form/react` and `@dimah-form/core` export helpful formatting utilities:

| Helper                           | Signature                                      | Description                                                                 |
| :------------------------------- | :--------------------------------------------- | :-------------------------------------------------------------------------- |
| **`fieldLabel(field)`**          | `(field: FormField) => string`                 | Returns `field.label ?? field.id`.                                          |
| **`fieldOptions(field)`**        | `(field: FormField) => FieldOption[]`          | Normalizes `{ value, label?, meta? }` arrays with label fallbacks.          |
| **`emptyToNull(value)`**         | `(value: unknown) => unknown`                  | Converts empty strings `""` to `null` so draft patches delete empty values. |
| **`formatAnswer(field, value)`** | `(field: FormField, value: unknown) => string` | Returns a human-friendly formatted string representation for summary views. |

***

## Advanced Pattern: Multi-Step Questionnaire Wizard [#advanced-pattern-multi-step-questionnaire-wizard]

You can organize long questionnaires into multi-step wizards using `field.meta.step` or splitting `q.visibleFields`:

```tsx title="components/questionnaire-wizard.tsx"
"use client";

import { useState } from "react";
import { useFormResponse } from "@/lib/form-client";
import { FormFieldControl } from "./form-field-control";
import type { FormSnapshot } from "@dimah-form/react";

export function QuestionnaireWizard({ form }: { form: FormSnapshot }) {
  const [currentStep, setCurrentStep] = useState(1);
  const q = useFormResponse({ snapshot: form });

  // Filter visible fields belonging to the current step (default step: 1)
  const stepFields = q.visibleFields.filter(
    (f) => (Number(f.meta?.step) || 1) === currentStep,
  );

  const totalSteps = Math.max(
    ...q.visibleFields.map((f) => Number(f.meta?.step) || 1),
  );

  return (
    <div className="mx-auto max-w-lg space-y-6">
      {/* Progress Bar */}
      <div>
        <div className="mb-1 flex justify-between text-xs text-gray-500">
          <span>
            Step {currentStep} of {totalSteps}
          </span>
          <span>{q.completion.percent}% Complete</span>
        </div>
        <div className="h-2 w-full overflow-hidden rounded-full bg-gray-200">
          <div
            className="h-full bg-black transition-all duration-300"
            style={{ width: `${q.completion.percent}%` }}
          />
        </div>
      </div>

      {/* Current Step Fields */}
      <div className="space-y-4">
        {stepFields.map((field) => (
          <FormFieldControl key={field.id} {...q.field(field.id)} />
        ))}
      </div>

      {/* Navigation Controls */}
      <div className="flex justify-between border-t pt-4">
        <button
          type="button"
          disabled={currentStep === 1}
          onClick={() => setCurrentStep((s) => s - 1)}
          className="rounded border px-4 py-2 disabled:opacity-30"
        >
          Previous
        </button>

        {currentStep < totalSteps ? (
          <button
            type="button"
            onClick={() => setCurrentStep((s) => s + 1)}
            className="rounded bg-black px-5 py-2 text-white"
          >
            Next Step
          </button>
        ) : (
          <button
            type="button"
            disabled={q.pending !== undefined}
            onClick={() => void q.submit()}
            className="rounded bg-green-600 px-5 py-2 text-white"
          >
            {q.pending === "submit" ? "Submitting..." : "Submit Questionnaire"}
          </button>
        )}
      </div>
    </div>
  );
}
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Custom Fields" href="/docs/custom-fields" description="Create custom server validators and render unique widgets like star ratings." />

  <Card title="Errors" href="/docs/errors" description="Customize error messages and display localized validation feedback." />

  <Card title="React Client" href="/docs/react" description="Review all useFormResponse options and reactive state properties." />
</Cards>


# Server (https://form.dimah.dev/docs/server)



`@dimah-form/server` is the Node / Edge engine: routing, snapshots, guards, and validation. Mount it on your framework, or call `form.api` in a server component with no extra HTTP hop.

***

## Initializing `dimahForm()` [#initializing-dimahform]

Create a centralized `dimahForm()` instance in your project:

```ts title="lib/form.ts"
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(), // or db(formDb) from @dimah-form/db
  forms,
  basePath: "/api/form", // Default base path
});

export type Form = typeof form;
```

`database` is required. A SQL database is not. `memoryAdapter()` is enough to run. For production SQL, see [Database](https://form.dimah.dev/docs/database).

***

## HTTP Framework Adapters [#http-framework-adapters]

`@dimah-form/server` provides dedicated adapters for all major JavaScript backend frameworks:

<Tabs items="[&#x22;Next.js&#x22;, &#x22;Hono&#x22;, &#x22;Express&#x22;, &#x22;Fastify&#x22;, &#x22;SvelteKit&#x22;, &#x22;Elysia&#x22;, &#x22;Node.js&#x22;, &#x22;Web Fetch&#x22;]">
  <Tab value="Next.js">
    Mount a catch-all route handler in the Next.js App Router:

    ```ts title="app/api/form/[...all]/route.ts"
    import { toNextJsHandler } from "@dimah-form/server/next";
    import { form } from "@/lib/form";

    export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(form);
    ```
  </Tab>

  <Tab value="Hono">
    ```ts title="src/index.ts"
    import { Hono } from "hono";
    import { toHonoHandler } from "@dimah-form/server/hono";
    import { form } from "./lib/form";

    const app = new Hono();

    app.on(
      ["GET", "POST", "PUT", "PATCH", "DELETE"],
      "/api/form/*",
      toHonoHandler(form),
    );

    export default app;
    ```
  </Tab>

  <Tab value="Express">
    ```ts title="src/server.ts"
    import express from "express";
    import { toExpressHandler } from "@dimah-form/server/express";
    import { form } from "./lib/form";

    const app = express();

    // Mount dimahForm BEFORE express.json() so it can read raw stream payloads
    app.all("/api/form/*", toExpressHandler(form));

    app.use(express.json());
    app.listen(3000, () => console.log("Server listening on port 3000"));
    ```
  </Tab>

  <Tab value="Fastify">
    ```ts title="src/server.ts"
    import Fastify from "fastify";
    import { toFastifyHandler } from "@dimah-form/server/fastify";
    import { form } from "./lib/form";

    const app = Fastify();

    app.all("/api/form/*", toFastifyHandler(form));

    await app.listen({ port: 3000 });
    ```
  </Tab>

  <Tab value="SvelteKit">
    ```ts title="src/routes/api/form/[...path]/+server.ts"
    import { toSvelteKitHandler } from "@dimah-form/server/svelte-kit";
    import { form } from "$lib/server/form";

    const handler = toSvelteKitHandler(form);

    export const GET = handler;
    export const POST = handler;
    export const PUT = handler;
    export const PATCH = handler;
    export const DELETE = handler;
    ```
  </Tab>

  <Tab value="Elysia">
    ```ts title="src/index.ts"
    import { Elysia } from "elysia";
    import { toElysiaHandler } from "@dimah-form/server/elysia";
    import { form } from "./lib/form";

    new Elysia().all("/api/form/*", toElysiaHandler(form)).listen(3000);
    ```
  </Tab>

  <Tab value="Node.js">
    ```ts title="src/server.ts"
    import { createServer } from "node:http";
    import { toNodeHandler } from "@dimah-form/server/node";
    import { form } from "./lib/form";

    const handler = toNodeHandler(form);

    const server = createServer((req, res) => {
      if (req.url?.startsWith("/api/form")) {
        return handler(req, res);
      }
      res.statusCode = 404;
      res.end("Not Found");
    });

    server.listen(3000);
    ```
  </Tab>

  <Tab value="Web Fetch">
    Use standard `Request` / `Response` directly (Cloudflare Workers, Deno, Bun):

    ```ts title="src/worker.ts"
    import { form } from "./lib/form";

    export default {
      fetch(request: Request) {
        if (new URL(request.url).pathname.startsWith("/api/form")) {
          return form.handler(request);
        }
        return new Response("Not Found", { status: 404 });
      },
    };
    ```
  </Tab>
</Tabs>

***

## Direct Server API (`form.api`) [#direct-server-api-formapi]

In addition to serving HTTP requests, `dimahForm()` provides a typed, internal API (`form.api`) that you can call directly in Next.js Server Actions, React Server Components, or backend services without any HTTP network overhead:

```tsx title="app/survey/[slug]/page.tsx"
import { form } from "@/lib/form";
import { notFound } from "next/navigation";
import { headers } from "next/headers";
import { Questionnaire } from "@/components/questionnaire";

interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function SurveyPage({ params }: PageProps) {
  const { slug } = await params;

  // Direct server call — runs validation and security guards internally
  const snapshot = await form.api.getForm({
    query: { formId: slug },
    headers: await headers(), // Forwards session cookies/headers to guard
  });

  if (!snapshot) {
    notFound();
  }

  return <Questionnaire form={snapshot} />;
}
```

### Available `form.api` Methods [#available-formapi-methods]

| Method                | Payload                                          | Returns          | Description                                         |
| :-------------------- | :----------------------------------------------- | :--------------- | :-------------------------------------------------- |
| **`getForm`**         | `{ query: { formId } }`                          | `FormSnapshot`   | Retrieves form definition by ID or slug.            |
| **`listForms`**       | `{ query?: { status?, limit?, offset? } }`       | `FormList`       | Returns paginated list of forms.                    |
| **`saveForm`**        | `{ body: FormSnapshot }`                         | `FormSnapshot`   | Upserts dynamic form in database.                   |
| **`deleteForm`**      | `{ body: { formId } }`                           | `{ ok: true }`   | Deletes a dynamic form (must have no responses).    |
| **`startResponse`**   | `{ body: { formId, respondentId?, resume? } }`   | `ResponseRecord` | Starts response session and freezes snapshot.       |
| **`getResponse`**     | `{ query: { responseId } }`                      | `ResponseRecord` | Fetches a response record.                          |
| **`listResponses`**   | `{ query?: ListResponsesQuery }`                 | `ResponseList`   | Lists response summaries or full records.           |
| **`saveDraft`**       | `{ body: { responseId, answers, updatedAt? } }`  | `ResponseRecord` | Patches partial answers on an active draft.         |
| **`submitResponse`**  | `{ body: { responseId, answers?, updatedAt? } }` | `ResponseRecord` | Validates visible answers and finalizes response.   |
| **`abandonResponse`** | `{ body: { responseId, updatedAt? } }`           | `ResponseRecord` | Marks draft as abandoned.                           |
| **`reopenResponse`**  | `{ body: { responseId, updatedAt? } }`           | `ResponseRecord` | Unlocks submitted/abandoned response back to draft. |
| **`deleteResponse`**  | `{ body: { responseId } }`                       | `{ ok: true }`   | Deletes response record.                            |

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Database" href="/docs/database" description="Required adapter: memory, optional FumaDB, or a custom store." />

  <Card title="Auth" href="/docs/auth" description="Secure form operations and respondent data with guard hooks." />

  <Card title="Configuration" href="/docs/configuration" description="Explore all available dimahForm configuration parameters." />
</Cards>


# Database (https://form.dimah.dev/docs/database)



`dimahForm({ database })` needs a `ResponseStore`. `@dimah-form/server` has no ORM. `@dimah-form/db` is optional — install it only when drafts must survive a restart.

| Adapter           | Package              | When                                             |
| :---------------- | :------------------- | :----------------------------------------------- |
| `memoryAdapter()` | `@dimah-form/server` | Tests, local, [Quickstart](https://form.dimah.dev/docs/quickstart)     |
| `db(formDb)`      | `@dimah-form/db`     | Production (FumaDB + Drizzle, Prisma, or Kysely) |
| Custom            | your code            | Implement [`ResponseStore`](https://form.dimah.dev/docs/configuration) |

***

## 1. In-memory [#1-in-memory]

No extra packages. Data lives in the process and is gone on restart.

```ts title="lib/form.ts"
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(),
  forms,
});
```

***

## 2. Production [#2-production]

Install `@dimah-form/db` and `fumadb` when you need SQL:

<Tabs items="[&#x22;pnpm&#x22;, &#x22;npm&#x22;, &#x22;yarn&#x22;, &#x22;bun&#x22;]">
  <Tab value="pnpm">
    `bash pnpm add @dimah-form/db fumadb `
  </Tab>

  <Tab value="npm">
    `bash npm install @dimah-form/db fumadb `
  </Tab>

  <Tab value="yarn">
    `bash yarn add @dimah-form/db fumadb `
  </Tab>

  <Tab value="bun">
    `bash bun add @dimah-form/db fumadb `
  </Tab>
</Tabs>

<Steps>
  <Step>
    ### Schema [#schema]

    Copy these tables, or generate with the [CLI](#cli) and re-add the indexes (FumaDB `generate` does not emit them).

    <Tabs items="[&#x22;Drizzle&#x22;, &#x22;Prisma&#x22;, &#x22;Kysely&#x22;]">
      <Tab value="Drizzle">
        ```ts title="lib/db/schema.ts"
        import { defineRelations } from "drizzle-orm";
        import {
          sqliteTable,
          text,
          blob,
          integer,
          foreignKey,
          index,
        } from "drizzle-orm/sqlite-core";

        export const questionnaire = sqliteTable(
          "questionnaire",
          {
            id: text("id", { length: 255 })
              .primaryKey()
              .notNull()
              .$defaultFn(() => crypto.randomUUID()),
            slug: text("slug", { length: 255 }).unique(),
            title: text("title").notNull(),
            definition: blob("definition", { mode: "json" }).notNull(),
            status: text("status").notNull(),
            createdAt: integer("created_at", { mode: "timestamp" })
              .notNull()
              .defaultNow(),
            updatedAt: integer("updated_at", { mode: "timestamp" })
              .notNull()
              .defaultNow(),
          },
          (table) => [
            index("questionnaire_status_updated_at_idx").on(
              table.status,
              table.updatedAt,
            ),
          ],
        );

        export const response = sqliteTable(
          "response",
          {
            id: text("id", { length: 255 })
              .primaryKey()
              .notNull()
              .$defaultFn(() => crypto.randomUUID()),
            questionnaireId: text("questionnaire_id", { length: 255 }).notNull(),
            status: text("status").notNull(),
            definition: blob("definition", { mode: "json" }).notNull(),
            answers: blob("answers", { mode: "json" }).notNull(),
            respondentId: text("respondent_id", { length: 255 }),
            submittedAt: integer("submitted_at", { mode: "timestamp" }),
            createdAt: integer("created_at", { mode: "timestamp" })
              .notNull()
              .defaultNow(),
            updatedAt: integer("updated_at", { mode: "timestamp" })
              .notNull()
              .defaultNow(),
          },
          (table) => [
            foreignKey({
              columns: [table.questionnaireId],
              foreignColumns: [questionnaire.id],
              name: "response_questionnaire_questionnaire_fk",
            })
              .onUpdate("restrict")
              .onDelete("restrict"),
            index("response_questionnaire_id_updated_at_idx").on(
              table.questionnaireId,
              table.updatedAt,
            ),
            index("response_respondent_id_updated_at_idx").on(
              table.respondentId,
              table.updatedAt,
            ),
            index("response_draft_lookup_idx").on(
              table.questionnaireId,
              table.respondentId,
              table.status,
              table.updatedAt,
            ),
          ],
        );

        export const private_dimah_form_settings = sqliteTable(
          "private_dimah_form_settings",
          {
            id: text("id", { length: 255 }).primaryKey().notNull(),
            version: text("version", { length: 255 }).notNull().default("1.0.0"),
          },
        );

        export const relations = defineRelations(
          { questionnaire, response, private_dimah_form_settings },
          (r) => ({
            questionnaire: {
              responses: r.many.response({
                alias: "response_questionnaire",
              }),
            },
            response: {
              questionnaire: r.one.questionnaire({
                from: r.response.questionnaireId,
                to: r.questionnaire.id,
                alias: "response_questionnaire",
              }),
            },
          }),
        );
        ```
      </Tab>

      <Tab value="Prisma">
        ```prisma title="prisma/schema.prisma"
        generator client {
          provider = "prisma-client-js"
        }

        datasource db {
          provider = "postgresql"
        }

        model Questionnaire {
          id         String   @id @default(cuid()) @map("id") @db.VarChar(255)
          slug       String?  @unique @map("slug") @db.VarChar(255)
          title      String   @map("title")
          definition Json     @map("definition")
          status     String   @map("status")
          createdAt  DateTime @default(now()) @map("created_at")
          updatedAt  DateTime @default(now()) @map("updated_at")
          responses  Response[] @relation("response_questionnaire")

          @@index([status, updatedAt], map: "questionnaire_status_updated_at_idx")
          @@map("questionnaire")
        }

        model Response {
          id              String    @id @default(cuid()) @map("id") @db.VarChar(255)
          questionnaireId String    @map("questionnaire_id") @db.VarChar(255)
          status          String    @map("status")
          definition      Json      @map("definition")
          answers         Json      @map("answers")
          respondentId    String?   @map("respondent_id") @db.VarChar(255)
          submittedAt     DateTime? @map("submitted_at")
          createdAt       DateTime  @default(now()) @map("created_at")
          updatedAt       DateTime  @default(now()) @map("updated_at")
          questionnaire   Questionnaire @relation("response_questionnaire", fields: [questionnaireId], references: [id], onUpdate: Restrict, onDelete: Restrict)

          @@index([questionnaireId, updatedAt], map: "response_questionnaire_id_updated_at_idx")
          @@index([respondentId, updatedAt], map: "response_respondent_id_updated_at_idx")
          @@index([questionnaireId, respondentId, status, updatedAt], map: "response_draft_lookup_idx")
          @@map("response")
        }

        model PrivateDimahFormSettings {
          id      String @id @map("id") @db.VarChar(255)
          version String @default("1.0.0") @map("version") @db.VarChar(255)

          @@map("private_dimah_form_settings")
        }
        ```
      </Tab>

      <Tab value="Kysely">
        Generate Kysely types with the CLI, then add these indexes:

        ```sql title="db/dimah-form-indexes.sql"
        CREATE INDEX IF NOT EXISTS questionnaire_status_updated_at_idx
          ON questionnaire (status, updated_at);

        CREATE INDEX IF NOT EXISTS response_questionnaire_id_updated_at_idx
          ON response (questionnaire_id, updated_at);

        CREATE INDEX IF NOT EXISTS response_respondent_id_updated_at_idx
          ON response (respondent_id, updated_at);

        CREATE INDEX IF NOT EXISTS response_draft_lookup_idx
          ON response (questionnaire_id, respondent_id, status, updated_at);
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### Client [#client]

    <Tabs items="[&#x22;Drizzle&#x22;, &#x22;Prisma&#x22;, &#x22;Kysely&#x22;]">
      <Tab value="Drizzle">
        ```ts title="lib/db.ts"
        import { createClient } from "@libsql/client";
        import { DimahFormDB } from "@dimah-form/db";
        import { drizzle } from "drizzle-orm/libsql";
        import { drizzleAdapter } from "fumadb/adapters/drizzle";
        import { relations } from "./schema";

        const sqlite = createClient({
          url: process.env.DATABASE_URL ?? "file:local.db",
        });

        export const formDb = DimahFormDB.client(
          drizzleAdapter({
            db: drizzle({ client: sqlite, relations }),
            provider: "sqlite",
          }),
        );
        ```
      </Tab>

      <Tab value="Prisma">
        ```ts title="lib/db.ts"
        import { PrismaClient } from "@prisma/client";
        import { DimahFormDB } from "@dimah-form/db";
        import { prismaAdapter } from "fumadb/adapters/prisma";

        const prisma = new PrismaClient();

        export const formDb = DimahFormDB.client(
          prismaAdapter({ prisma, provider: "postgresql" }),
        );
        ```
      </Tab>

      <Tab value="Kysely">
        ```ts title="lib/db.ts"
        import { DimahFormDB } from "@dimah-form/db";
        import { kyselyAdapter } from "fumadb/adapters/kysely";
        import { db } from "./kysely";

        export const formDb = DimahFormDB.client(
          kyselyAdapter({ db, provider: "postgresql" }),
        );
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### Connect [#connect]

    ```ts title="lib/form.ts"
    import { db } from "@dimah-form/db";
    import { dimahForm } from "@dimah-form/server";
    import { formDb } from "@/lib/db";
    import { forms } from "@/lib/forms";

    export const form = dimahForm({
      database: db(formDb),
      forms,
    });

    export type Form = typeof form;
    ```
  </Step>
</Steps>

***

## CLI [#cli]

```ts title="scripts/db-cli.mts"
import { DimahFormDB } from "@dimah-form/db";
import { runCli } from "@dimah-form/db/cli";
import { drizzleAdapter } from "fumadb/adapters/drizzle";

void runCli(
  DimahFormDB.client(drizzleAdapter({ db: {} as never, provider: "sqlite" })),
);
```

```bash
node --import tsx scripts/db-cli.mts generate latest -o ./lib/schema.ts
```

`generate` overwrites the file. Re-add these indexes afterward:

| Index                                      | Columns                                                     |
| ------------------------------------------ | ----------------------------------------------------------- |
| `questionnaire_status_updated_at_idx`      | `status`, `updated_at`                                      |
| `response_questionnaire_id_updated_at_idx` | `questionnaire_id`, `updated_at`                            |
| `response_respondent_id_updated_at_idx`    | `respondent_id`, `updated_at`                               |
| `response_draft_lookup_idx`                | `questionnaire_id`, `respondent_id`, `status`, `updated_at` |

***

## Custom store [#custom-store]

Implement [`ResponseStore`](https://form.dimah.dev/docs/configuration) and pass it to `dimahForm({ database })` directly. Do not wrap it in `db()`.

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Snapshots" href="/docs/snapshots" description="How snapshots and CAS updatedAt are stored." />

  <Card title="Auth" href="/docs/auth" description="Protect records with guard." />

  <Card title="Configuration" href="/docs/configuration" description="The ResponseStore interface." />
</Cards>


# Auth (https://form.dimah.dev/docs/auth)



Auth is not a library feature. Put it in `guard`. Use `on*` / `after*` hooks to stamp `respondentId` and fire emails or webhooks after a successful write.

<Flow
  label="Security & Execution Pipeline"
  steps="[
  {
    name: &#x22;1. Guard&#x22;,
    kind: &#x22;server&#x22;,
    note: &#x22;Authenticate & check permissions&#x22;,
  },
  {
    name: &#x22;2. Validate&#x22;,
    kind: &#x22;protocol&#x22;,
    note: &#x22;Validate answers vs snapshot&#x22;,
  },
  {
    name: &#x22;3. on* Hook&#x22;,
    kind: &#x22;server&#x22;,
    note: &#x22;Pre-write mutation (e.g. set respondentId)&#x22;,
  },
  { name: &#x22;4. Persist&#x22;, kind: &#x22;data&#x22;, note: &#x22;Atomic database write & CAS&#x22; },
  {
    name: &#x22;5. after* Hook&#x22;,
    kind: &#x22;server&#x22;,
    note: &#x22;Post-write side effects (email, webhook)&#x22;,
  },
]"
/>

***

## The `guard` Security Hook [#the-guard-security-hook]

The `guard` hook executes before any database operation or endpoint handler. If `guard` throws an `APIError`, the request is immediately rejected:

```ts title="lib/form.ts"
import {
  APIError,
  FORM_ERROR_CODES,
  dimahForm,
  memoryAdapter,
} from "@dimah-form/server";
import { forms } from "@/lib/forms";
import { getAuthSession } from "@/lib/auth";

export const form = dimahForm({
  database: memoryAdapter(),
  forms,
  guard: async ({ request, operation, formId, responseId, getResponse }) => {
    // 1. Authenticate user from session cookies or Bearer token
    const session = await getAuthSession(request);

    // 2. Restrict administrative endpoints to Admins
    const adminOperations = ["saveForm", "deleteForm", "listResponses"];
    if (adminOperations.includes(operation)) {
      if (!session || session.user.role !== "admin") {
        throw APIError.from("FORBIDDEN", FORM_ERROR_CODES.FORBIDDEN);
      }
    }

    // 3. Enforce Row-Level Respondent Ownership on response mutations
    if (responseId) {
      const record = await getResponse(responseId);

      if (record && record.respondentId) {
        if (!session || session.user.id !== record.respondentId) {
          throw APIError.from("FORBIDDEN", FORM_ERROR_CODES.FORBIDDEN);
        }
      }
    }
  },
});
```

### Context Passed to `guard` [#context-passed-to-guard]

| Argument          | Type                                                   | Description                                                                 |
| :---------------- | :----------------------------------------------------- | :-------------------------------------------------------------------------- |
| **`request`**     | `Request`                                              | Standard Fetch `Request` object with headers, cookies, and URL.             |
| **`operation`**   | `FormApiOperation \| string`                           | Name of the active operation (e.g., `"startResponse"`, `"submitResponse"`). |
| **`formId`**      | `string \| undefined`                                  | Form identifier if present in the query or body payload.                    |
| **`responseId`**  | `string \| undefined`                                  | Response identifier if present in the query or body payload.                |
| **`getResponse`** | `(id: string) => Promise<ResponseRecord \| undefined>` | Helper to fetch the current response record from the store.                 |

***

## Sending Client Credentials & Headers [#sending-client-credentials--headers]

Configure `createFormClient` to automatically include auth tokens or session cookies with every request:

```ts title="lib/form-client.ts"
"use client";

import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";
import { getClientToken } from "@/lib/auth-client";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
  credentials: "include", // Include browser session cookies
  headers: async () => {
    const token = await getClientToken();
    return token ? { Authorization: `Bearer ${token}` } : {};
  },
});
```

***

## Pre-Write & Post-Write Lifecycle Hooks [#pre-write--post-write-lifecycle-hooks]

Lifecycle hooks allow you to mutate records before persistence or run side-effects after persistence:

```ts title="lib/form.ts"
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { sendEmailNotification } from "@/lib/notifications";
import { getAuthSession } from "@/lib/auth";

export const form = dimahForm({
  database: memoryAdapter(),
  hooks: {
    // 1. Pre-write: Runs before creating a new response in DB
    onStart: async ({ request, response }) => {
      const session = await getAuthSession(request);
      if (session) {
        response.respondentId = session.user.id;
      }
    },

    // 2. Pre-write: Runs before saving a draft
    onDraft: async ({ request, response, patch }) => {
      console.log(`User updating draft for response ${response.id}`);
    },

    // 3. Post-write: Runs after successful submit transaction
    afterSubmit: async ({ response }) => {
      await sendEmailNotification({
        to: "team@company.com",
        subject: `New response submitted for form ${response.formId}`,
        responseId: response.id,
      });
    },

    // 4. Post-write: Runs after response is deleted
    afterDelete: async ({ responseId }) => {
      console.log(`Response ${responseId} was deleted.`);
    },
  },
});
```

### Hook Execution Rules [#hook-execution-rules]

* **Pre-write (`on*`)**: Synchronously or asynchronously mutates the `response` record before the database write commits. If an error is thrown, the write is aborted.
* **Post-write (`after*`)**: Executes after the database write has succeeded. Safe for external network calls, transactional emails, or message queues.

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Errors" href="/docs/errors" description="Review UNAUTHORIZED, FORBIDDEN, and standard error shapes." />

  <Card title="Plugins" href="/docs/plugins" description="Learn how plugins can contribute custom endpoints and guard checks." />

  <Card title="React Client" href="/docs/react" description="Pass credentials and handle authentication errors in useFormResponse." />
</Cards>


# Custom Fields (https://form.dimah.dev/docs/custom-fields)



Custom field types are **server validators**, not React components. You define how the answer is checked and typed. You render whatever control you want.

***

## 1. Defining a Custom Field Type [#1-defining-a-custom-field-type]

Use `defineFieldType()` from `@dimah-form/core` to declare a new field validator:

```ts title="lib/field-types/rating.ts"
import { defineFieldType } from "@dimah-form/core";
import * as z from "zod";

export const ratingFieldType = defineFieldType({
  type: "rating",

  // 1. Zod schema for validating the field's configuration in defineForm()
  fieldSchema: z.looseObject({
    type: z.literal("rating"),
    min: z.number().int().min(1).default(1),
    max: z.number().int().max(10).default(5),
  }),

  // 2. Server validator for respondent answer values
  validate: (value, field) => {
    if (typeof value !== "number" || !Number.isInteger(value)) {
      return {
        message: "Rating must be an integer",
        code: "EXPECTED_INTEGER",
      };
    }

    const min = field.min ?? 1;
    const max = field.max ?? 5;

    if (value < min) {
      return {
        message: `Rating must be at least ${min}`,
        code: "TOO_SMALL",
        params: { min },
      };
    }

    if (value > max) {
      return {
        message: `Rating cannot exceed ${max}`,
        code: "TOO_LARGE",
        params: { max },
      };
    }

    return undefined; // Valid!
  },

  // 3. Helper to determine if the field is empty for `required` checks
  isEmpty: (value) => value == null,

  // 4. Type token for TypeScript compile-time answer inference
  $Infer: 0 as number,
});
```

Export your custom field types from a central module:

```ts title="lib/field-types/index.ts"
import { ratingFieldType } from "./rating";

export const customFieldTypes = [ratingFieldType] as const;
```

***

## 2. Registering on Server and Client [#2-registering-on-server-and-client]

Register your custom field types in both `dimahForm()` and `createFormClient()`:

### Server Registration [#server-registration]

```ts title="lib/form.ts"
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { customFieldTypes } from "@/lib/field-types";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(),
  fieldTypes: customFieldTypes,
  forms,
});

export type Form = typeof form;
```

### Client Registration [#client-registration]

```ts title="lib/form-client.ts"
"use client";

import { createFormClient } from "@dimah-form/react";
import { customFieldTypes } from "@/lib/field-types";
import type { Form } from "@/lib/form";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
  fieldTypes: customFieldTypes,
});

export const { useFormResponse } = formClient;
```

***

## 3. Using Custom Fields in Forms [#3-using-custom-fields-in-forms]

Define questionnaires with your new field type:

```ts title="lib/forms/review.ts"
import { defineForm } from "@dimah-form/server";

export const reviewForm = defineForm({
  title: "Product Review",
  slug: "review",
  fields: [
    {
      id: "score",
      type: "rating",
      label: "Star Rating (1-5)",
      required: true,
      min: 1,
      max: 5,
    },
    {
      id: "comments",
      type: "text",
      label: "Review Comments",
    },
  ],
});
```

TypeScript automatically infers `Form["$Infer"]["answers"]["review"]["score"]` as `number`.

***

## 4. Rendering Custom Widgets in React [#4-rendering-custom-widgets-in-react]

Add a branch to your `FormFieldControl` dispatcher to render an interactive star rating control:

```tsx title="components/form-field-control.tsx"
import { type FormFieldBinding } from "@dimah-form/react";

export function FormFieldControl(binding: FormFieldBinding) {
  const { field, value, error, disabled, required, onChange } = binding;
  if (!field) return null;

  // Custom Rating Widget
  if (field.type === "rating") {
    const currentRating = typeof value === "number" ? value : 0;
    const max = typeof field.max === "number" ? field.max : 5;

    return (
      <div className="space-y-1.5">
        <label className="block text-sm font-semibold">
          {field.label}
          {required && <span className="ml-1 text-red-500">*</span>}
        </label>
        <div className="flex gap-1">
          {Array.from({ length: max }, (_, i) => i + 1).map((star) => (
            <button
              key={star}
              type="button"
              disabled={disabled}
              onClick={() => onChange(star)}
              className={`text-2xl transition-colors ${
                star <= currentRating ? "text-yellow-400" : "text-gray-300"
              }`}
            >
              ★
            </button>
          ))}
        </div>
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // Other field types...
  return null;
}
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Field Types" href="/docs/field-types" description="Explore all 7 built-in field types and showWhen conditional rules." />

  <Card title="Widgets" href="/docs/widgets" description="Discover patterns for rendering complex, interactive inputs." />

  <Card title="Plugins" href="/docs/plugins" description="Bundle custom field types and custom endpoints into reusable plugins." />
</Cards>


# Plugins (https://form.dimah.dev/docs/plugins)



A plugin can add HTTP endpoints, lifecycle hooks, error codes, field types, and matching methods on `createFormClient`.

***

## 1. Creating a Server Plugin [#1-creating-a-server-plugin]

Use `definePlugin()` and `createFormEndpoint()` from `@dimah-form/server` to build a server extension:

```ts title="lib/plugins/analytics-plugin.ts"
import {
  createFormEndpoint,
  defineErrorCodes,
  definePlugin,
} from "@dimah-form/server";
import * as z from "zod";

// 1. Declare domain-specific error codes
export const ANALYTICS_ERROR_CODES = defineErrorCodes({
  STATS_UNAVAILABLE: "Analytics data could not be computed",
});

// 2. Define the server plugin
export const analyticsPlugin = () =>
  definePlugin({
    id: "analytics",
    $ERROR_CODES: ANALYTICS_ERROR_CODES,

    // Add custom HTTP endpoints to the dimahForm router
    endpoints: {
      getFormStats: createFormEndpoint(
        "/analytics/stats",
        {
          method: "GET",
          query: z.object({
            formId: z.string(),
          }),
        },
        async ({ query, store, request }) => {
          const list = await store.listResponses({
            formId: query.formId,
            limit: 100,
          });

          const total = list.responses.length;
          const submitted = list.responses.filter(
            (r) => r.status === "submitted",
          ).length;

          return {
            formId: query.formId,
            totalResponses: total,
            completionRate: total > 0 ? (submitted / total) * 100 : 0,
          };
        },
      ),
    },

    // Optional lifecycle hooks contributed by this plugin
    hooks: {
      afterSubmit: async ({ response }) => {
        console.log(`[Analytics] Response ${response.id} submitted.`);
      },
    },
  });
```

***

## 2. Registering on the Server [#2-registering-on-the-server]

Pass the plugin to `dimahForm({ plugins: [...] })`:

```ts title="lib/form.ts"
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { analyticsPlugin } from "@/lib/plugins/analytics-plugin";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(),
  forms,
  plugins: [analyticsPlugin()],
});

export type Form = typeof form;
```

You can now call the plugin endpoint directly through `form.api`:

```ts
const stats = await form.api.getFormStats({
  query: { formId: "feedback" },
});
```

***

## 3. Creating a Companion Client Plugin [#3-creating-a-companion-client-plugin]

To expose typed methods on `createFormClient`, create a companion client plugin using `defineClientPlugin()`:

```ts title="lib/plugins/analytics-client.ts"
import { defineClientPlugin } from "@dimah-form/react";
import { ANALYTICS_ERROR_CODES } from "./analytics-plugin";

export const analyticsClientPlugin = () =>
  defineClientPlugin({
    id: "analytics",
    $ERROR_CODES: ANALYTICS_ERROR_CODES,
    endpoints: ({ $fetch }) => ({
      getStats: (formId: string) =>
        $fetch<{
          formId: string;
          totalResponses: number;
          completionRate: number;
        }>(`/analytics/stats?formId=${encodeURIComponent(formId)}`, {
          method: "GET",
        }),
    }),
  });
```

Register the client plugin with `createFormClient`:

```ts title="lib/form-client.ts"
"use client";

import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";
import { analyticsClientPlugin } from "@/lib/plugins/analytics-client";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
  plugins: [analyticsClientPlugin()],
});

// Call the plugin method from client components
const stats = await formClient.getStats("feedback");
```

***

## Plugin Options Reference [#plugin-options-reference]

| Property           | Type                           | Description                                                          |
| :----------------- | :----------------------------- | :------------------------------------------------------------------- |
| **`id`**           | `string`                       | Unique alphanumeric identifier for the plugin.                       |
| **`$ERROR_CODES`** | `ErrorCodeCatalog`             | Custom error codes declared via `defineErrorCodes()`.                |
| **`endpoints`**    | `Record<string, FormEndpoint>` | Map of custom route handlers.                                        |
| **`hooks`**        | `DimahFormHooks`               | Pre-write and post-write lifecycle hooks.                            |
| **`fieldTypes`**   | `FieldTypeDefinition[]`        | Custom field validators added by this plugin.                        |
| **`dependsOn`**    | `string[]`                     | Array of required plugin IDs that must be loaded before this plugin. |

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Auth" href="/docs/auth" description="Learn how guard hooks secure plugin endpoints." />

  <Card title="Custom Fields" href="/docs/custom-fields" description="Package reusable validators inside plugins." />

  <Card title="Errors" href="/docs/errors" description="Understand how custom error codes merge with core error catalogs." />
</Cards>


# Configuration (https://form.dimah.dev/docs/configuration)



Options for the server instance, the React client, the fill hook, and a custom `database` adapter.

***

## `dimahForm(config)` [#dimahformconfig]

The primary server instance initializer from `@dimah-form/server`. `database` is required. `@dimah-form/db` is not — `memoryAdapter()` ships in `@dimah-form/server`.

```ts
import { dimahForm } from "@dimah-form/server";

export const form = dimahForm({
  database: memoryAdapter(),
  forms: { ... },
  basePath: "/api/form",
});
```

### Configuration Options [#configuration-options]

| Option                | Type                                  | Required | Default       | Description                                                                                                               |
| :-------------------- | :------------------------------------ | :------- | :------------ | :------------------------------------------------------------------------------------------------------------------------ |
| **`database`**        | `ResponseStore`                       | **Yes**  | —             | Persistence adapter (`memoryAdapter()`, `db(formDb)` from `@dimah-form/db`, or a custom `ResponseStore`).                 |
| **`forms`**           | `Record<string, FormDefinitionInput>` | No       | `{}`          | Catalog of code-authored forms created with `defineForm()`. Feeds `$Infer`.                                               |
| **`fieldTypes`**      | `FieldTypeDefinition[]`               | No       | `[]`          | Custom field type definitions created with `defineFieldType()`.                                                           |
| **`basePath`**        | `string`                              | No       | `"/api/form"` | Base path prefix for HTTP route mounting.                                                                                 |
| **`guard`**           | `DimahFormGuard`                      | No       | `undefined`   | Security callback executed before every query or mutation.                                                                |
| **`hooks`**           | `DimahFormHooks`                      | No       | `{}`          | Pre-write (`onStart`, `onDraft`, `onSubmit`) and post-write (`afterSubmit`, `afterDraft`, `afterDelete`) lifecycle hooks. |
| **`validateAnswers`** | `AnswersValidator`                    | No       | `undefined`   | Custom cross-field validation function running on submit.                                                                 |
| **`plugins`**         | `DimahFormPlugin[]`                   | No       | `[]`          | Server plugins created with `definePlugin()`.                                                                             |
| **`metaSchema`**      | `DimahFormMetaSchema`                 | No       | `undefined`   | Custom Zod schemas for validating `meta` dictionaries on forms and fields.                                                |

***

## `createFormClient<T>(options)` [#createformclienttoptions]

Initializes the typed client runtime from `@dimah-form/react`.

```ts
import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
});
```

### Configuration Options [#configuration-options-1]

| Option            | Type                                               | Default            | Description                                                                             |
| :---------------- | :------------------------------------------------- | :----------------- | :-------------------------------------------------------------------------------------- |
| **`basePath`**    | `string`                                           | `"/api/form"`      | Relative URL path prefix matching the server's `basePath`.                              |
| **`baseURL`**     | `string`                                           | `undefined`        | Absolute URL prefix (e.g., `"https://api.example.com/api/form"`). Overrides `basePath`. |
| **`credentials`** | `RequestCredentials`                               | `"same-origin"`    | Fetch credentials policy (e.g. `"include"` for cross-origin cookies).                   |
| **`headers`**     | `HeadersInit \| (() => MaybePromise<HeadersInit>)` | `undefined`        | Static headers or dynamic header generator for auth tokens.                             |
| **`fieldTypes`**  | `FieldTypeDefinition[]`                            | `[]`               | Custom field types matching the server configuration.                                   |
| **`plugins`**     | `DimahFormClientPlugin[]`                          | `[]`               | Companion client plugins created with `defineClientPlugin()`.                           |
| **`fetch`**       | `typeof fetch`                                     | `globalThis.fetch` | Custom fetch implementation for SSR or testing harnesses.                               |

***

## `useFormResponse(options)` [#useformresponseoptions]

Headless React hook for managing fill sessions.

### Options [#options]

| Option             | Type                                 | Required | Description                                                          |
| :----------------- | :----------------------------------- | :------- | :------------------------------------------------------------------- |
| **`snapshot`**     | `FormSnapshot`                       | **Yes**  | Form definition snapshot (retrieved from server).                    |
| **`response`**     | `ResponseRecord`                     | No       | Existing response row to edit or view.                               |
| **`respondentId`** | `string \| (() => string)`           | No       | User identifier for draft ownership.                                 |
| **`resume`**       | `boolean`                            | No       | If `true`, reconnects to the user's latest draft on first write.     |
| **`autosave`**     | `boolean \| { debounceMs?: number }` | No       | Enables debounced background draft saving (default debounce: 600ms). |
| **`validate`**     | `"submit" \| "change"`               | No       | `"submit"` (default) or `"change"` (real-time validation on edit).   |
| **`onStarted`**    | `(record: ResponseRecord) => void`   | No       | Callback invoked after `startResponse` completes.                    |
| **`onSaved`**      | `(record: ResponseRecord) => void`   | No       | Callback invoked after `saveDraft` completes.                        |
| **`onSubmitted`**  | `(record: ResponseRecord) => void`   | No       | Callback invoked after `submitResponse` completes.                   |
| **`onReopened`**   | `(record: ResponseRecord) => void`   | No       | Callback invoked after `reopenResponse` completes.                   |
| **`onAbandoned`**  | `(record: ResponseRecord) => void`   | No       | Callback invoked after `abandonResponse` completes.                  |

***

## The `ResponseStore` Interface [#the-responsestore-interface]

Any database adapter passed to `dimahForm({ database })` must satisfy the following interface:

```ts
interface ResponseStore {
  // Form Definitions
  getForm(idOrSlug: string): MaybePromise<FormSnapshot | undefined>;
  saveForm(
    form: FormSnapshot,
    options?: { expectedUpdatedAt?: string },
  ): MaybePromise<void>;
  deleteForm(id: string): MaybePromise<void>;
  listForms(query?: {
    status?: FormStatus;
    limit?: number;
    offset?: number;
  }): MaybePromise<{
    forms: FormSnapshot[];
    limit: number;
    offset: number;
    nextOffset: number | null;
  }>;

  // Response Sessions
  create(row: ResponseRecord): MaybePromise<void>;
  get(id: string): MaybePromise<ResponseRecord | undefined>;
  save(
    row: ResponseRecord,
    options?: { expectedUpdatedAt?: string },
  ): MaybePromise<void>;
  delete(id: string): MaybePromise<void>;
  listResponses(query?: ListResponsesQuery): MaybePromise<{
    responses: (ResponseRecord | ResponseSummary)[];
    limit: number;
    offset: number;
    nextOffset: number | null;
  }>;

  // Draft Helpers
  findLatestDraft(query: {
    formId: string;
    respondentId: string;
  }): MaybePromise<ResponseRecord | undefined>;
  getOrCreateDraft(row: ResponseRecord): MaybePromise<ResponseRecord>;
}
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Field Types Reference" href="/docs/field-types" description="Explore built-in field types, validation rules, and showWhen operators." />

  <Card title="Protocol" href="/docs/protocol" description="Review REST route schemas, parameters, and payload structures." />

  <Card title="Error Codes" href="/docs/errors" description="Inspect machine-readable error codes and field validation issues." />
</Cards>


# Field Types (https://form.dimah.dev/docs/field-types)



Seven built-in types in `@dimah-form/core`. Each one has a definition schema, a server validator, and an `$Infer` mapping.

***

## Built-In Field Types Catalog [#built-in-field-types-catalog]

| Type              | Answer Type | Schema Constraints                                                             | Description                                                                                                     |
| :---------------- | :---------- | :----------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- |
| **`text`**        | `string`    | `minLength?: number`<br />`maxLength?: number`<br />`pattern?: string` (regex) | Single-line or multi-line text input. Validates length and regex pattern.                                       |
| **`number`**      | `number`    | `min?: number`<br />`max?: number`<br />`integer?: boolean`                    | Numeric input. Validates minimum, maximum, and integer constraints.                                             |
| **`boolean`**     | `boolean`   | —                                                                              | Checkbox or toggle switch. Values are strictly `true` or `false`.                                               |
| **`select`**      | `string`    | `options: SelectOption[]`                                                      | Single-choice dropdown or radio group. Answer must match an option `value`.                                     |
| **`multiSelect`** | `string[]`  | `options: SelectOption[]`                                                      | Multi-choice checkbox group or tag selector. Answer must be an array of valid option values without duplicates. |
| **`email`**       | `string`    | —                                                                              | Email address input. Validates standard RFC 5322 email formatting.                                              |
| **`date`**        | `string`    | `min?: string` (YYYY-MM-DD)<br />`max?: string` (YYYY-MM-DD)                   | Calendar date in `YYYY-MM-DD` ISO format. Validates date boundaries.                                            |

***

## Select Option Structure [#select-option-structure]

Options passed to `select` or `multiSelect` fields follow this schema:

```ts
type SelectOption = {
  /** The persisted answer value (e.g., "us", "uk", "de") */
  value: string;

  /** Display label for the UI (defaults to value if omitted) */
  label?: string;

  /** Custom metadata for icons, descriptions, or badge tags */
  meta?: Record<string, unknown>;
};
```

***

## Conditional Visibility (`showWhen`) [#conditional-visibility-showwhen]

The `showWhen` property dynamically hides or shows fields based on the respondent's answers.

<Callout type="info">
  **Automatic Cleanup**: When a field is conditionally hidden, its answer is
  automatically stripped from the submitted response so outdated answers never
  persist to your database.
</Callout>

### 1. Leaf Conditions [#1-leaf-conditions]

#### `equals` [#equals]

Matches when the target field's answer strictly equals the specified value:

```ts
{
  id: "githubHandle",
  type: "text",
  label: "GitHub Handle",
  showWhen: { field: "role", equals: "developer" },
}
```

#### `notEquals` [#notequals]

Matches when the target field's answer is not equal to the specified value:

```ts
{
  id: "reason",
  type: "text",
  label: "Reason for cancellation",
  showWhen: { field: "plan", notEquals: "free" },
}
```

#### `includes` [#includes]

Matches when a `multiSelect` array answer contains the specified value:

```ts
{
  id: "reactExperience",
  type: "number",
  label: "Years of React Experience",
  showWhen: { field: "skills", includes: "react" },
}
```

### 2. Compound Conditions (`all` / `any`) [#2-compound-conditions-all--any]

#### `all` (Logical AND) [#all-logical-and]

Matches only when **all** nested conditions evaluate to `true`:

```ts
showWhen: {
  all: [
    { field: "department", equals: "engineering" },
    { field: "isLead", equals: true },
  ],
}
```

#### `any` (Logical OR) [#any-logical-or]

Matches when **at least one** nested condition evaluates to `true`:

```ts
showWhen: {
  any: [
    { field: "country", equals: "US" },
    { field: "country", equals: "CA" },
  ],
}
```

***

## Type Inference (`$Infer`) Mapping [#type-inference-infer-mapping]

When defining forms with `defineForm()`, TypeScript derives the answer type automatically:

```ts title="lib/forms/example.ts"
import { defineForm } from "@dimah-form/server";

export const exampleForm = defineForm({
  title: "Example",
  slug: "example",
  fields: [
    { id: "name", type: "text", required: true },
    { id: "age", type: "number" }, // Optional because required is omitted
    {
      id: "role",
      type: "select",
      required: true,
      options: [{ value: "admin" }, { value: "user" }],
    },
    {
      id: "tags",
      type: "multiSelect",
      options: [{ value: "frontend" }, { value: "backend" }],
    },
  ],
});
```

The resulting `$Infer` answer type is strictly typed:

```ts
type ExampleAnswers = Form["$Infer"]["answers"]["example"];

// Resolves to:
// {
//   name: string;
//   role: "admin" | "user";
//   age?: number;
//   tags?: ("frontend" | "backend")[];
// }
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Custom Fields" href="/docs/custom-fields" description="Create custom validators and $Infer type mappings with defineFieldType." />

  <Card title="Widgets" href="/docs/widgets" description="Render UI widgets for all 7 built-in field types." />

  <Card title="Protocol" href="/docs/protocol" description="Inspect request and response payload schemas." />
</Cards>


# Protocol (https://form.dimah.dev/docs/protocol)



Route paths, payloads, and response shapes live in `@dimah-form/core`. Default base path: `/api/form`.

***

## Form Management Endpoints [#form-management-endpoints]

| Method | Path           | Client Method                             | `form.api` Method      | Description                                      |
| :----- | :------------- | :---------------------------------------- | :--------------------- | :----------------------------------------------- |
| `GET`  | `/form`        | `getForm({ formId })`                     | `getForm({ query })`   | Retrieve form definition snapshot by ID or slug. |
| `POST` | `/form`        | `saveForm(form)`                          | `saveForm({ body })`   | Upsert dynamic form definition in database.      |
| `POST` | `/form/delete` | `deleteForm({ formId })`                  | `deleteForm({ body })` | Delete form (rejected if responses exist).       |
| `GET`  | `/forms`       | `listForms({ status?, limit?, offset? })` | `listForms({ query })` | List forms (default limit: 50, max: 100).        |

***

## Response Lifecycle Endpoints [#response-lifecycle-endpoints]

| Method | Path                | Client Method                                                         | `form.api` Method           | Description                                                 |
| :----- | :------------------ | :-------------------------------------------------------------------- | :-------------------------- | :---------------------------------------------------------- |
| `POST` | `/response/start`   | `startResponse({ formId, respondentId?, resume? })`                   | `startResponse({ body })`   | Start fill session and freeze definition snapshot.          |
| `GET`  | `/response`         | `getResponse({ responseId })`                                         | `getResponse({ query })`    | Retrieve a full response record by ID.                      |
| `GET`  | `/responses`        | `listResponses({ formId?, respondentId?, status?, limit?, offset? })` | `listResponses({ query })`  | List response summaries (or full rows with `include=full`). |
| `POST` | `/response/draft`   | `saveDraft({ responseId, answers, updatedAt? })`                      | `saveDraft({ body })`       | Save partial answers patch on active draft.                 |
| `POST` | `/response/submit`  | `submitResponse({ responseId, answers?, updatedAt? })`                | `submitResponse({ body })`  | Validate answers vs snapshot and finalize submission.       |
| `POST` | `/response/abandon` | `abandonResponse({ responseId, updatedAt? })`                         | `abandonResponse({ body })` | Lock draft response as abandoned.                           |
| `POST` | `/response/reopen`  | `reopenResponse({ responseId, updatedAt? })`                          | `reopenResponse({ body })`  | Unlock submitted/abandoned response back to draft.          |
| `POST` | `/response/delete`  | `deleteResponse({ responseId })`                                      | `deleteResponse({ body })`  | Delete a response record.                                   |

***

## Endpoint Details & Payloads [#endpoint-details--payloads]

### 1. `POST /response/start` [#1-post-responsestart]

Starts a new fill session for a questionnaire.

* **Request Body**:
  ```json
  {
    "formId": "feedback",
    "respondentId": "user-123",
    "resume": true
  }
  ```
* **Response Body (`200 OK`)**:
  ```json
  {
    "id": "resp_01h8x...",
    "formId": "feedback",
    "status": "draft",
    "definition": {
      "id": "feedback",
      "slug": "feedback",
      "title": "Product Feedback",
      "fields": [...]
    },
    "answers": {},
    "respondentId": "user-123",
    "submittedAt": null,
    "createdAt": "2026-09-19T10:00:00.000Z",
    "updatedAt": "2026-09-19T10:00:00.000Z"
  }
  ```

***

### 2. `POST /response/draft` [#2-post-responsedraft]

Saves a partial patch of answers. Setting a key to `null` deletes that answer.

* **Request Body**:
  ```json
  {
    "responseId": "resp_01h8x...",
    "answers": {
      "fullName": "Jane Doe",
      "rating": "great"
    },
    "updatedAt": "2026-09-19T10:00:00.000Z"
  }
  ```
* **Response Body (`200 OK`)**: Updated `ResponseRecord`.

***

### 3. `POST /response/submit` [#3-post-responsesubmit]

Validates all visible required answers against the snapshot schema and locks the response to `"submitted"`.

* **Request Body**:
  ```json
  {
    "responseId": "resp_01h8x...",
    "answers": {
      "fullName": "Jane Doe",
      "rating": "poor",
      "improvements": "Please improve loading speed."
    },
    "updatedAt": "2026-09-19T10:05:00.000Z"
  }
  ```
* **Response Body (`200 OK`)**: Updated `ResponseRecord` with `status: "submitted"`.

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Errors" href="/docs/errors" description="Inspect machine-readable error codes returned on failed requests." />

  <Card title="Snapshots" href="/docs/snapshots" description="Learn how snapshots ensure safe draft lifecycles." />

  <Card title="Configuration" href="/docs/configuration" description="View full client and server options." />
</Cards>


# Errors (https://form.dimah.dev/docs/errors)



Failed requests return JSON with a stable `code`, an English `message`, and optional `issues`.

***

## Standard Error Response Schema [#standard-error-response-schema]

```ts
type ErrorResponseBody = {
  /** Human-readable explanation of the error */
  message: string;

  /** Stable machine-readable error code */
  code?: string;

  /** Optional parameters for message interpolation */
  params?: Record<string, string | number>;

  /** Field-level validation issues (present on VALIDATION_ERROR) */
  issues?: Array<{
    field: string;
    message: string;
    code?: string;
    params?: Record<string, string | number>;
  }>;
};
```

***

## Core Error Codes (`FORM_ERROR_CODES`) [#core-error-codes-form_error_codes]

| Error Code                       | HTTP Status | Trigger Condition                                                                  |
| :------------------------------- | :---------- | :--------------------------------------------------------------------------------- |
| **`NOT_FOUND`**                  | 404         | Requested URL route does not match any registered endpoint.                        |
| **`UNKNOWN_FORM`**               | 404         | Form ID or slug could not be found in code catalogs or database.                   |
| **`UNKNOWN_RESPONSE`**           | 404         | Response ID does not exist in the database.                                        |
| **`UNAUTHORIZED`**               | 401         | Thrown by `guard` when the user is unauthenticated.                                |
| **`FORBIDDEN`**                  | 403         | Thrown by `guard` when the user lacks permissions or ownership.                    |
| **`STALE_UPDATE`**               | 409         | `updatedAt` CAS token mismatch (concurrent write collision).                       |
| **`FORM_INACTIVE`**              | 409         | `startResponse` called on a form with status `"draft"` or `"archived"`.            |
| **`RESPONSE_NOT_DRAFT`**         | 409         | `saveDraft` or `submitResponse` attempted on a locked response.                    |
| **`RESPONSE_NOT_LOCKED`**        | 409         | `reopenResponse` attempted on an active draft response.                            |
| **`CODE_AUTHORED_FORM`**         | 409         | Overwrite (`saveForm`) or delete (`deleteForm`) attempted on a code-authored form. |
| **`FORM_HAS_RESPONSES`**         | 409         | `deleteForm` attempted on a form that already has recorded responses.              |
| **`SLUG_TAKEN`**                 | 409         | Form slug collides with an existing form.                                          |
| **`VALIDATION_ERROR`**           | 400         | Payload or answer validation failure (inspect `issues[]`).                         |
| **`UNKNOWN_FIELD_TYPE`**         | 400         | Form references a field type not registered in `fieldTypes`.                       |
| **`RESUME_REQUIRES_RESPONDENT`** | 400         | `startResponse({ resume: true })` called without a `respondentId`.                 |
| **`INTERNAL_ERROR`**             | 500         | Unhandled server exception.                                                        |

***

## Field Issue Codes (`FIELD_ISSUE_CODES`) [#field-issue-codes-field_issue_codes]

When a `400 VALIDATION_ERROR` occurs during submission, the `issues[]` array contains field-specific issue codes:

| Issue Code                  | Description                                          | Parameters |
| :-------------------------- | :--------------------------------------------------- | :--------- |
| **`REQUIRED`**              | Visible required field was left empty on submit.     | —          |
| **`UNKNOWN_FIELD`**         | Answer key does not exist in the form snapshot.      | —          |
| **`UNKNOWN_FIELD_TYPE`**    | Field type has no registered validator.              | `{ type }` |
| **`INVALID`**               | Custom validator rejected the answer.                | —          |
| **`EXPECTED_STRING`**       | Answer must be a string.                             | —          |
| **`EXPECTED_NUMBER`**       | Answer must be a finite number.                      | —          |
| **`EXPECTED_INTEGER`**      | Answer must be an integer.                           | —          |
| **`EXPECTED_BOOLEAN`**      | Answer must be a boolean (`true` or `false`).        | —          |
| **`EXPECTED_STRING_ARRAY`** | Answer must be an array of strings.                  | —          |
| **`EXPECTED_EMAIL`**        | Answer must be a valid email address.                | —          |
| **`EXPECTED_DATE`**         | Answer must be an ISO calendar date (`YYYY-MM-DD`).  | —          |
| **`TOO_SHORT`**             | Text length is below `minLength`.                    | `{ min }`  |
| **`TOO_LONG`**              | Text length exceeds `maxLength`.                     | `{ max }`  |
| **`TOO_SMALL`**             | Numeric value is below `min`.                        | `{ min }`  |
| **`TOO_LARGE`**             | Numeric value exceeds `max`.                         | `{ max }`  |
| **`INVALID_FORMAT`**        | Text does not match the configured `pattern` regex.  | —          |
| **`INVALID_OPTION`**        | Selected value is not in the allowed `options` list. | —          |
| **`DUPLICATE_OPTION`**      | `multiSelect` array contains duplicate values.       | —          |

***

## Client Error Helpers [#client-error-helpers]

`@dimah-form/react` provides helpers to map error codes to user-friendly or localized messages:

```ts
import {
  fieldIssueMap,
  formErrorMessage,
  issuesByField,
} from "@dimah-form/react";

// Convert APIError issues array into a field-id-to-message map
const issues = fieldIssueMap(apiError);
// { fullName: "Full Name is required", email: "Invalid email" }

// Extract formatted top-level error message
const message = formErrorMessage(error);
```

***

## Declaring Custom Error Codes with `defineErrorCodes` [#declaring-custom-error-codes-with-defineerrorcodes]

When authoring plugins or custom field validators, use `defineErrorCodes()` to declare type-safe custom error catalogs:

```ts
import { defineErrorCodes } from "@dimah-form/server";

export const CUSTOM_ERROR_CODES = defineErrorCodes({
  PAYMENT_REQUIRED: "Payment verification failed",
  SUBSCRIPTION_EXPIRED: "Account subscription is inactive",
});
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Auth" href="/docs/auth" description="Learn how to throw UNAUTHORIZED and FORBIDDEN errors in guards." />

  <Card title="Protocol" href="/docs/protocol" description="Review all route endpoints and HTTP status codes." />

  <Card title="React Client" href="/docs/react" description="Inspect how useFormResponse handles field error states." />
</Cards>
